@effected/config-file 0.13.0 → 0.14.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/ConfigEvent.js CHANGED
@@ -21,15 +21,14 @@ const ConfigSourceRef = Schema.Struct({
21
21
  * persistence.
22
22
  *
23
23
  * @remarks
24
- * v3's `Stringified` and `ResolutionFailed` variants were declared and never
25
- * emitted; they are not ported. `DiscoveryFailed` goes with them: under this
26
- * package's resolver-absorption contract a resolver's error channel is `never`
27
- * — every filesystem failure becomes `Option.none()` — so the pipeline can
28
- * never observe a discovery failure to report one.
24
+ * There is no discovery-failure variant: under this package's
25
+ * resolver-absorption contract a resolver's error channel is `never` — every
26
+ * filesystem failure becomes `Option.none()` — so the pipeline never observes a
27
+ * discovery failure to report.
29
28
  *
30
29
  * The failure variants carry the **structured** typed error in `error`, not a
31
- * `reason` string. v3 stringified them, destroying every field a subscriber
32
- * might branch on; a subscriber that wants prose can read `error.message`.
30
+ * `reason` string, so every field a subscriber might branch on survives; a
31
+ * subscriber that wants prose can read `error.message`.
33
32
  *
34
33
  * @public
35
34
  */
package/ConfigFile.js CHANGED
@@ -10,7 +10,7 @@ import { Context, DateTime, Effect, FileSystem, Layer, Option, Path, PubSub, Sch
10
10
  * @remarks
11
11
  * Its own tag, so "no config anywhere" is routable with `Effect.catchTag`
12
12
  * separately from "the config I found is broken" — the single most important
13
- * distinction the v3 mega-error could not express.
13
+ * distinction in the error set.
14
14
  *
15
15
  * @public
16
16
  */
@@ -40,8 +40,8 @@ var ConfigFileNotFoundError = class extends Schema.TaggedError()("ConfigFileNotF
40
40
  * Indicates that a config file could not be read from the filesystem.
41
41
  *
42
42
  * @remarks
43
- * `cause` preserves the underlying filesystem failure structurally. v3 flattened
44
- * it to `reason: String(e)`.
43
+ * `cause` preserves the underlying filesystem failure structurally rather than
44
+ * flattening it to a string.
45
45
  *
46
46
  * @public
47
47
  */
@@ -75,13 +75,12 @@ var ConfigFileWriteError = class extends Schema.TaggedError()("ConfigFileWriteEr
75
75
  * was called on a service configured without a `defaultPath`.
76
76
  *
77
77
  * @remarks
78
- * v3 reported this as a generic `ConfigError` carrying `operation: "save"` and
79
- * `reason: "no default path configured"` — indistinguishable by tag from a real
80
- * write failure.
78
+ * It is distinct by tag from a real {@link ConfigFileWriteError}, so a caller
79
+ * can tell "no destination was configured" from "the write failed".
81
80
  *
82
81
  * It carries no `path` field on purpose. The whole point of this failure is
83
82
  * that there is no path; a {@link ConfigFileWriteError} with a fabricated path
84
- * would be a lie, and lying error payloads are what this port exists to undo.
83
+ * would be a lie.
85
84
  *
86
85
  * @public
87
86
  */
@@ -96,10 +95,9 @@ var ConfigDefaultPathMissingError = class extends Schema.TaggedError()("ConfigDe
96
95
  *
97
96
  * @remarks
98
97
  * `issue` carries the **structured** schema failure — at runtime a
99
- * `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues`. v3
100
- * flattened this to `String(ParseError)`, destroying every field a caller might
101
- * branch on. It is typed `unknown` because v4 exposes no `Schema` for `Issue`;
102
- * narrow it with the `SchemaIssue` module.
98
+ * `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues`, so
99
+ * every field a caller might branch on survives. It is typed `unknown` because
100
+ * there is no `Schema` for `Issue`; narrow it with the `SchemaIssue` module.
103
101
  *
104
102
  * @public
105
103
  */
@@ -244,10 +242,10 @@ const makeImpl = (options, fs, path, resolverEnv) => {
244
242
  const searched = options.resolvers.map((r) => r.name);
245
243
  /**
246
244
  * Merge the discovered sources and announce the result. Shared by `load` and
247
- * `loadOrDefault`, which in v3 each re-inlined it and drifted apart.
245
+ * `loadOrDefault`, so the two cannot drift apart.
248
246
  *
249
- * Both events carry EVERY contributing source. v3 reported `sources[0].path`,
250
- * which is wrong under `layeredMerge`, where all of them contributed.
247
+ * Both events carry EVERY contributing source, because under `layeredMerge`
248
+ * all of them contributed.
251
249
  */
252
250
  const mergeAndEmit = (sources) => Effect.gen(function* () {
253
251
  const value = yield* options.strategy.resolve(sources);
@@ -331,10 +329,10 @@ const makeImpl = (options, fs, path, resolverEnv) => {
331
329
  * Resolve `defaultPath`, `mkdir -p` its parent and write. Shared by `save`
332
330
  * and `update`, and — crucially — emits nothing.
333
331
  *
334
- * v3's `update` called the public `save`, so a single `update` published
335
- * `Written` + `Saved` + `Updated`; v3 documented this as a known smell. Event
336
- * granularity is per-operation, so the emitting boundary must sit in the
337
- * public method, never in the shared internals it delegates to.
332
+ * `update` must not call the public `save`, or one `update` would publish
333
+ * `Saved` as well as `Updated`. Event granularity is per-operation, so the
334
+ * emitting boundary sits in the public method, never in the shared internals
335
+ * it delegates to.
338
336
  */
339
337
  const saveTo = (value) => Effect.gen(function* () {
340
338
  const configured = options.defaultPath;
@@ -459,14 +457,30 @@ var ConfigFile = class {
459
457
  * Build the live layer for a config service class.
460
458
  *
461
459
  * @remarks
462
- * Resolver requirements flow into the layer's `R` type. v3 cast them away with
463
- * `as Effect.Effect<Option<string>>`, making `Layer<Service, never, FileSystem>`
464
- * a claim rather than a proof.
460
+ * Resolver requirements flow into the layer's `R` type: the result is
461
+ * `Layer<Self, never, FileSystem | Path | RR>`, so the platform services and
462
+ * every resolver's needs are visible at the provide site.
465
463
  *
466
464
  * `ConfigFile.layer` is a layer-RETURNING function, not a layer: calling it
467
465
  * twice builds two independent service instances. Bind its result to a const
468
- * and provide that const, per the memoization discipline — do not call
469
- * `ConfigFile.layer(...)` inline at each provide site.
466
+ * and provide that const — do not call `ConfigFile.layer(...)` inline at each
467
+ * provide site.
468
+ *
469
+ * @example
470
+ * ```ts
471
+ * import { ConfigFile, ConfigResolver, JsonCodec, MergeStrategy } from "@effected/config-file";
472
+ * import { Schema } from "effect";
473
+ *
474
+ * const AppShape = Schema.Struct({ port: Schema.Number });
475
+ * class AppConfig extends ConfigFile.Service<AppConfig, typeof AppShape.Type>()("app/Config") {}
476
+ *
477
+ * const AppConfigLive = ConfigFile.layer(AppConfig, {
478
+ * schema: AppShape,
479
+ * codec: JsonCodec,
480
+ * resolvers: [ConfigResolver.explicitPath("./app.config.json")],
481
+ * strategy: MergeStrategy.firstMatch<typeof AppShape.Type>(),
482
+ * });
483
+ * ```
470
484
  */
471
485
  static layer = layer;
472
486
  /**
@@ -485,10 +499,9 @@ var ConfigFile = class {
485
499
  * directory is created through `FileSystem.makeTempDirectory` rather than
486
500
  * `node:fs`.
487
501
  *
488
- * `Layer.scoped` does not exist in v4. `Layer.effect` types its layer as
489
- * `Layer<I, E, Exclude<R, Scope>>`, so an `Effect.addFinalizer` inside it binds
490
- * to the layer's own scope and runs on release without surfacing `Scope` in the
491
- * layer's requirements.
502
+ * The temp directory is removed by a finalizer bound to the layer's own scope,
503
+ * so cleanup runs on release without surfacing `Scope` in the layer's
504
+ * requirements.
492
505
  *
493
506
  * @example
494
507
  * ```ts
@@ -524,10 +537,17 @@ var ConfigFile = class {
524
537
  * @example
525
538
  * ```ts
526
539
  * import { ConfigFile, JsonCodec } from "@effected/config-file";
540
+ * import { Effect, Schema } from "effect";
541
+ *
542
+ * const MyConfig = Schema.Struct({ port: Schema.Number });
527
543
  *
528
- * const config = yield* ConfigFile.read(inputs.configFile, {
529
- * schema: MyConfig,
530
- * codec: JsonCodec,
544
+ * // Requires `FileSystem` in `R`; provide it from a platform layer.
545
+ * const program = Effect.gen(function* () {
546
+ * const config = yield* ConfigFile.read("./app.config.json", {
547
+ * schema: MyConfig,
548
+ * codec: JsonCodec,
549
+ * });
550
+ * return config.port;
531
551
  * });
532
552
  * ```
533
553
  */
@@ -7,8 +7,7 @@ import { Effect, Schema } from "effect";
7
7
  * @remarks
8
8
  * `phase` says where: reading the current version, applying a step, or writing
9
9
  * the new version back. `cause` preserves the underlying failure by identity
10
- * when the failing step signals recoverable failure with `Effect.fail`. v3
11
- * assembled all three into a prose `reason` string.
10
+ * when the failing step signals recoverable failure with `Effect.fail`.
12
11
  *
13
12
  * @public
14
13
  */
@@ -90,6 +89,23 @@ var ConfigMigration = class {
90
89
  * {@link ConfigMigrationError} rather than flattening migration failures into
91
90
  * the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
92
91
  * in its error type.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { ConfigMigration, JsonCodec } from "@effected/config-file";
96
+ * import { Effect } from "effect";
97
+ *
98
+ * const codec = ConfigMigration.make({
99
+ * codec: JsonCodec,
100
+ * migrations: [
101
+ * {
102
+ * version: 2,
103
+ * name: "rename-host",
104
+ * up: (raw) => Effect.succeed({ ...(raw as object), host: (raw as { hostname?: string }).hostname }),
105
+ * },
106
+ * ],
107
+ * });
108
+ * ```
93
109
  */
94
110
  static make = make;
95
111
  };
package/ConfigProvider.js CHANGED
@@ -2,22 +2,21 @@ import { ConfigProvider, Effect } from "effect";
2
2
 
3
3
  //#region src/ConfigProvider.ts
4
4
  /**
5
- * Expose a loaded, merged, schema-validated document as a v4 `ConfigProvider`,
5
+ * Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
6
6
  * so it can be read through `Config.String("port")` and layered beneath other
7
7
  * providers.
8
8
  *
9
9
  * @remarks
10
10
  * Strictly additive, and deliberately in its own module so it never becomes a
11
11
  * required import. The schema-validated whole-document
12
- * {@link ConfigFileShape.load} remains the primary API: v4's `Config` has no
12
+ * {@link ConfigFileShape.load} remains the primary API: `Config` has no
13
13
  * whole-document story, and this function is the bridge, not a replacement.
14
14
  *
15
15
  * Three properties, in order of how easy they are to lose:
16
16
  *
17
17
  * 1. **A missing file is a failure, not an empty provider.** The
18
18
  * {@link ConfigFileNotFoundError} propagates. Handing back an empty provider
19
- * would silently turn every subsequent `Config` read into "not found", which
20
- * is the exact class of lie this port exists to undo.
19
+ * would silently turn every subsequent `Config` read into "not found".
21
20
  * 2. **Nested keys are structural, not dotted.** `fromUnknown` descends one path
22
21
  * segment at a time, so `{ db: { host } }` is reached with
23
22
  * `Config.nested(Config.String("host"), "db")` — never `Config.String("db.host")`,
@@ -45,8 +44,14 @@ import { ConfigProvider, Effect } from "effect";
45
44
  *
46
45
  * @example
47
46
  * ```ts
48
- * const fileProvider = yield* asConfigProvider(cfg);
49
- * const provider = ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider);
47
+ * import { asConfigProvider } from "@effected/config-file";
48
+ * import { ConfigProvider, Effect } from "effect";
49
+ *
50
+ * const program = Effect.gen(function* () {
51
+ * const cfg = yield* AppConfig; // a ConfigFile.Service class
52
+ * const fileProvider = yield* asConfigProvider(cfg);
53
+ * return ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider);
54
+ * });
50
55
  * ```
51
56
  *
52
57
  * @public
@@ -57,9 +62,8 @@ const asConfigProvider = (service) => Effect.map(service.load, (value) => Config
57
62
  * `ConfigProvider`, so `Config` accessors read env first and the file second.
58
63
  *
59
64
  * @remarks
60
- * This is the composition the v3 library could not express. This layer
61
- * composes beneath the **ambient** `ConfigProvider`, v4's `Context.Reference`
62
- * for it, so a consumer supplying `ConfigProvider.layer(...)` controls
65
+ * This layer composes beneath the **ambient** `ConfigProvider` (a
66
+ * `Context.Reference`), so a consumer supplying `ConfigProvider.layer(...)` controls
63
67
  * precedence explicitly: whatever that provider resolves wins, and the config
64
68
  * file supplies whatever it lacks. That reference's own default is
65
69
  * `ConfigProvider.fromEnv()`, which is what most applications want; this
package/EncryptedCodec.js CHANGED
@@ -7,10 +7,9 @@ import { Duration, Effect, Exit, Schema } from "effect";
7
7
  * failed.
8
8
  *
9
9
  * @remarks
10
- * Its own error rather than v3's `"key-derivation"` value on the generic
11
- * `ConfigCodecError.operation` union — an encryption-only concern was leaking
12
- * into every codec's error type. `cause` preserves the underlying host failure
13
- * structurally; v3 assembled it into a prose `reason` string.
10
+ * Its own error rather than a value on the generic `ConfigCodecError.operation`
11
+ * union, so an encryption-only concern does not leak into every codec's error
12
+ * type. `cause` preserves the underlying host failure structurally.
14
13
  *
15
14
  * @public
16
15
  */
@@ -88,6 +87,14 @@ const keyEffect = (keySource) => keySource._tag === "CryptoKey" ? keySource.key
88
87
  * flattening — the inner codec's failures stay distinguishable from
89
88
  * cryptographic ones.
90
89
  *
90
+ * @example
91
+ * ```ts
92
+ * import { EncryptedCodec, EncryptedCodecKey, JsonCodec } from "@effected/config-file";
93
+ *
94
+ * const salt = new Uint8Array(16); // use a stored, random per-deployment salt
95
+ * const codec = EncryptedCodec(JsonCodec, EncryptedCodecKey.fromPassphrase("correct horse", salt));
96
+ * ```
97
+ *
91
98
  * @public
92
99
  */
93
100
  function EncryptedCodec(inner, keySource) {
package/MergeStrategy.js CHANGED
@@ -36,11 +36,19 @@ const layeredMerge = () => ({
36
36
  }
37
37
  });
38
38
  /**
39
- * Built-in merge strategies.
39
+ * Built-in merge strategies: `firstMatch` (the highest-priority source wins
40
+ * whole) and `layeredMerge` (every source deep-merged, higher priority winning).
40
41
  *
41
42
  * @remarks
42
- * Renamed from v3's `ConfigWalkStrategy`, which never walked anything — the
43
- * thing that walks is the `upwardWalk` resolver.
43
+ * Strategies only combine sources; the walk up the directory tree belongs to
44
+ * the `upwardWalk` resolver.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { MergeStrategy } from "@effected/config-file";
49
+ *
50
+ * const strategy = MergeStrategy.layeredMerge<{ port: number }>();
51
+ * ```
44
52
  *
45
53
  * @public
46
54
  */
package/README.md CHANGED
@@ -7,10 +7,10 @@
7
7
 
8
8
  Composable config file loading for Effect. Declare a resolver chain — an explicit path, an upward walk from the cwd, the workspace or git root, `/etc` — decode every discovered file through an Effect `Schema`, and combine the results with a merge strategy. JSON, JSONC, YAML and TOML all decode with no extra install. Codecs, resolvers and merge strategies are pluggable seams, and failures arrive as tagged errors carrying structured payloads rather than prose, so "no config anywhere" is routable separately from "the config I found is broken".
9
9
 
10
- > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
- > development against a single pinned Effect v4 prerelease. Packages graduate to
12
- > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
- > exactly the ones the kit is built and tested against, install
10
+ > **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
11
+ > Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
12
+ > makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
13
+ > `@effect/*` versions on the line the kit is built and tested against, install
14
14
  > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
15
  >
16
16
  > **Stability: unstable.** This package's API surface is not yet considered
package/index.d.ts CHANGED
@@ -60,8 +60,11 @@ export declare class ConfigCodecError extends ConfigCodecError_base {
60
60
  * @public
61
61
  */
62
62
  interface ConfigCodec<E = ConfigCodecError> {
63
+ /** The codec's name, e.g. `"json"`; recorded on {@link ConfigCodecError} and in events. */
63
64
  readonly name: string;
65
+ /** Turn file content into an unknown document. */
64
66
  readonly parse: (raw: string) => Effect.Effect<unknown, E>;
67
+ /** Turn a document back into file content. */
65
68
  readonly stringify: (value: unknown) => Effect.Effect<string, E>;
66
69
  }
67
70
  //#endregion
@@ -86,15 +89,14 @@ export declare const ConfigSourceRef: Schema.Struct<{
86
89
  * persistence.
87
90
  *
88
91
  * @remarks
89
- * v3's `Stringified` and `ResolutionFailed` variants were declared and never
90
- * emitted; they are not ported. `DiscoveryFailed` goes with them: under this
91
- * package's resolver-absorption contract a resolver's error channel is `never`
92
- * — every filesystem failure becomes `Option.none()` — so the pipeline can
93
- * never observe a discovery failure to report one.
92
+ * There is no discovery-failure variant: under this package's
93
+ * resolver-absorption contract a resolver's error channel is `never` — every
94
+ * filesystem failure becomes `Option.none()` — so the pipeline never observes a
95
+ * discovery failure to report.
94
96
  *
95
97
  * The failure variants carry the **structured** typed error in `error`, not a
96
- * `reason` string. v3 stringified them, destroying every field a subscriber
97
- * might branch on; a subscriber that wants prose can read `error.message`.
98
+ * `reason` string, so every field a subscriber might branch on survives; a
99
+ * subscriber that wants prose can read `error.message`.
98
100
  *
99
101
  * @public
100
102
  */
@@ -260,7 +262,9 @@ export declare class ConfigEvents extends ConfigEvents_base {
260
262
  * @public
261
263
  */
262
264
  export interface ConfigResolver<R = never> {
265
+ /** The resolver's name, reported in `searched` lists, events and `ConfigSource.resolver`. */
263
266
  readonly name: string;
267
+ /** Look up the config file's path: `Option.some(path)` when found, `Option.none()` otherwise. */
264
268
  readonly resolve: Effect.Effect<Option.Option<string>, never, R>;
265
269
  /**
266
270
  * The same lookup, reporting **how** the file was found rather than only
@@ -511,15 +515,25 @@ type NonEmptySources<A> = readonly [ConfigSource<A>, ...ConfigSource<A>[]];
511
515
  * @public
512
516
  */
513
517
  export interface MergeStrategy<A> {
518
+ /** The strategy's name, reported on the `Resolved` event. */
514
519
  readonly name: string;
520
+ /** Combine the sources, highest priority first, into one value. */
515
521
  readonly resolve: (sources: NonEmptySources<A>) => Effect.Effect<A>;
516
522
  }
517
523
  /**
518
- * Built-in merge strategies.
524
+ * Built-in merge strategies: `firstMatch` (the highest-priority source wins
525
+ * whole) and `layeredMerge` (every source deep-merged, higher priority winning).
519
526
  *
520
527
  * @remarks
521
- * Renamed from v3's `ConfigWalkStrategy`, which never walked anything — the
522
- * thing that walks is the `upwardWalk` resolver.
528
+ * Strategies only combine sources; the walk up the directory tree belongs to
529
+ * the `upwardWalk` resolver.
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * import { MergeStrategy } from "@effected/config-file";
534
+ *
535
+ * const strategy = MergeStrategy.layeredMerge<{ port: number }>();
536
+ * ```
523
537
  *
524
538
  * @public
525
539
  */
@@ -551,7 +565,7 @@ declare const ConfigFileNotFoundError_base: Schema.Class<ConfigFileNotFoundError
551
565
  * @remarks
552
566
  * Its own tag, so "no config anywhere" is routable with `Effect.catchTag`
553
567
  * separately from "the config I found is broken" — the single most important
554
- * distinction the v3 mega-error could not express.
568
+ * distinction in the error set.
555
569
  *
556
570
  * @public
557
571
  */
@@ -568,8 +582,8 @@ declare const ConfigFileReadError_base: Schema.Class<ConfigFileReadError, Schema
568
582
  * Indicates that a config file could not be read from the filesystem.
569
583
  *
570
584
  * @remarks
571
- * `cause` preserves the underlying filesystem failure structurally. v3 flattened
572
- * it to `reason: String(e)`.
585
+ * `cause` preserves the underlying filesystem failure structurally rather than
586
+ * flattening it to a string.
573
587
  *
574
588
  * @public
575
589
  */
@@ -596,13 +610,12 @@ declare const ConfigDefaultPathMissingError_base: Schema.Class<ConfigDefaultPath
596
610
  * was called on a service configured without a `defaultPath`.
597
611
  *
598
612
  * @remarks
599
- * v3 reported this as a generic `ConfigError` carrying `operation: "save"` and
600
- * `reason: "no default path configured"` — indistinguishable by tag from a real
601
- * write failure.
613
+ * It is distinct by tag from a real {@link ConfigFileWriteError}, so a caller
614
+ * can tell "no destination was configured" from "the write failed".
602
615
  *
603
616
  * It carries no `path` field on purpose. The whole point of this failure is
604
617
  * that there is no path; a {@link ConfigFileWriteError} with a fabricated path
605
- * would be a lie, and lying error payloads are what this port exists to undo.
618
+ * would be a lie.
606
619
  *
607
620
  * @public
608
621
  */
@@ -621,10 +634,9 @@ declare const ConfigValidationError_base: Schema.Class<ConfigValidationError, Sc
621
634
  *
622
635
  * @remarks
623
636
  * `issue` carries the **structured** schema failure — at runtime a
624
- * `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues`. v3
625
- * flattened this to `String(ParseError)`, destroying every field a caller might
626
- * branch on. It is typed `unknown` because v4 exposes no `Schema` for `Issue`;
627
- * narrow it with the `SchemaIssue` module.
637
+ * `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues`, so
638
+ * every field a caller might branch on survives. It is typed `unknown` because
639
+ * there is no `Schema` for `Issue`; narrow it with the `SchemaIssue` module.
628
640
  *
629
641
  * @public
630
642
  */
@@ -723,7 +735,7 @@ interface ConfigFileShape<A> {
723
735
  * @remarks
724
736
  * A found-but-corrupt source ABORTS discovery with a typed error rather than
725
737
  * being silently skipped: silently skipping a corrupt file would mean running
726
- * on the wrong config. This is deliberate, and is parity with v3.
738
+ * on the wrong config. This is deliberate.
727
739
  */
728
740
  readonly discover: Effect.Effect<ReadonlyArray<ConfigSource<A>>, ConfigReadError>;
729
741
  /**
@@ -786,7 +798,7 @@ interface ConfigFileOptions<A, I, RR> {
786
798
  * The schema every discovered document is decoded through.
787
799
  *
788
800
  * @remarks
789
- * `Schema.Codec<A, I>` rather than v4's one-parameter `Schema.Schema<A>`,
801
+ * `Schema.Codec<A, I>` rather than the one-parameter `Schema.Schema<A>`,
790
802
  * because the encoded form `I` matters on the write path. Its decoding and
791
803
  * encoding service channels default to `never`, keeping `decode` free of
792
804
  * requirements.
@@ -830,8 +842,6 @@ interface ConfigFileOptions<A, I, RR> {
830
842
  *
831
843
  * @remarks
832
844
  * Its requirements join the resolvers' in `RR` and flow into the layer's `R`.
833
- * v3 typed this `Effect<string, ConfigError, any>` and cast the requirements
834
- * away at the call site.
835
845
  *
836
846
  * When absent, `save` and `update` fail with
837
847
  * {@link ConfigDefaultPathMissingError}.
@@ -931,14 +941,30 @@ export declare class ConfigFile {
931
941
  * Build the live layer for a config service class.
932
942
  *
933
943
  * @remarks
934
- * Resolver requirements flow into the layer's `R` type. v3 cast them away with
935
- * `as Effect.Effect<Option<string>>`, making `Layer<Service, never, FileSystem>`
936
- * a claim rather than a proof.
944
+ * Resolver requirements flow into the layer's `R` type: the result is
945
+ * `Layer<Self, never, FileSystem | Path | RR>`, so the platform services and
946
+ * every resolver's needs are visible at the provide site.
937
947
  *
938
948
  * `ConfigFile.layer` is a layer-RETURNING function, not a layer: calling it
939
949
  * twice builds two independent service instances. Bind its result to a const
940
- * and provide that const, per the memoization discipline — do not call
941
- * `ConfigFile.layer(...)` inline at each provide site.
950
+ * and provide that const — do not call `ConfigFile.layer(...)` inline at each
951
+ * provide site.
952
+ *
953
+ * @example
954
+ * ```ts
955
+ * import { ConfigFile, ConfigResolver, JsonCodec, MergeStrategy } from "@effected/config-file";
956
+ * import { Schema } from "effect";
957
+ *
958
+ * const AppShape = Schema.Struct({ port: Schema.Number });
959
+ * class AppConfig extends ConfigFile.Service<AppConfig, typeof AppShape.Type>()("app/Config") {}
960
+ *
961
+ * const AppConfigLive = ConfigFile.layer(AppConfig, {
962
+ * schema: AppShape,
963
+ * codec: JsonCodec,
964
+ * resolvers: [ConfigResolver.explicitPath("./app.config.json")],
965
+ * strategy: MergeStrategy.firstMatch<typeof AppShape.Type>(),
966
+ * });
967
+ * ```
942
968
  */
943
969
  static readonly layer: <Self, A, I, RR = never>(tag: Context.Key<Self, ConfigFileShape<A>>, options: ConfigFileOptions<A, I, RR>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path | RR>;
944
970
  /**
@@ -957,10 +983,9 @@ export declare class ConfigFile {
957
983
  * directory is created through `FileSystem.makeTempDirectory` rather than
958
984
  * `node:fs`.
959
985
  *
960
- * `Layer.scoped` does not exist in v4. `Layer.effect` types its layer as
961
- * `Layer<I, E, Exclude<R, Scope>>`, so an `Effect.addFinalizer` inside it binds
962
- * to the layer's own scope and runs on release without surfacing `Scope` in the
963
- * layer's requirements.
986
+ * The temp directory is removed by a finalizer bound to the layer's own scope,
987
+ * so cleanup runs on release without surfacing `Scope` in the layer's
988
+ * requirements.
964
989
  *
965
990
  * @example
966
991
  * ```ts
@@ -996,10 +1021,17 @@ export declare class ConfigFile {
996
1021
  * @example
997
1022
  * ```ts
998
1023
  * import { ConfigFile, JsonCodec } from "@effected/config-file";
1024
+ * import { Effect, Schema } from "effect";
1025
+ *
1026
+ * const MyConfig = Schema.Struct({ port: Schema.Number });
999
1027
  *
1000
- * const config = yield* ConfigFile.read(inputs.configFile, {
1001
- * schema: MyConfig,
1002
- * codec: JsonCodec,
1028
+ * // Requires `FileSystem` in `R`; provide it from a platform layer.
1029
+ * const program = Effect.gen(function* () {
1030
+ * const config = yield* ConfigFile.read("./app.config.json", {
1031
+ * schema: MyConfig,
1032
+ * codec: JsonCodec,
1033
+ * });
1034
+ * return config.port;
1003
1035
  * });
1004
1036
  * ```
1005
1037
  */
@@ -1023,8 +1055,7 @@ declare const ConfigMigrationError_base: Schema.Class<ConfigMigrationError, Sche
1023
1055
  * @remarks
1024
1056
  * `phase` says where: reading the current version, applying a step, or writing
1025
1057
  * the new version back. `cause` preserves the underlying failure by identity
1026
- * when the failing step signals recoverable failure with `Effect.fail`. v3
1027
- * assembled all three into a prose `reason` string.
1058
+ * when the failing step signals recoverable failure with `Effect.fail`.
1028
1059
  *
1029
1060
  * @public
1030
1061
  */
@@ -1035,13 +1066,14 @@ export declare class ConfigMigrationError extends ConfigMigrationError_base {
1035
1066
  * A single versioned migration step.
1036
1067
  *
1037
1068
  * @remarks
1038
- * v3 also declared a `down` reverse migration. It was never invoked anywhere in
1039
- * the codebase, so it is not ported.
1069
+ * Steps are forward-only: there is no reverse (`down`) migration.
1040
1070
  *
1041
1071
  * @public
1042
1072
  */
1043
1073
  interface ConfigFileMigration {
1074
+ /** The version this step migrates the config to. Steps run in ascending order. */
1044
1075
  readonly version: number;
1076
+ /** A label for the step, carried on {@link ConfigMigrationError} when it fails. */
1045
1077
  readonly name: string;
1046
1078
  /**
1047
1079
  * Transforms the parsed config. Signal recoverable failure with `Effect.fail`;
@@ -1051,7 +1083,9 @@ interface ConfigFileMigration {
1051
1083
  }
1052
1084
  /** How the version number is read from and written to the parsed config. @public */
1053
1085
  export interface VersionAccess {
1086
+ /** Read the current version from the parsed config. */
1054
1087
  readonly get: (raw: unknown) => Effect.Effect<number, unknown>;
1088
+ /** Return the config with `version` written back. */
1055
1089
  readonly set: (raw: unknown, version: number) => Effect.Effect<unknown, unknown>;
1056
1090
  }
1057
1091
  /** Reads and writes a top-level `version` field. @public */
@@ -1060,8 +1094,11 @@ export declare const VersionAccess: {
1060
1094
  };
1061
1095
  /** Options for {@link ConfigMigration.make}. @public */
1062
1096
  interface ConfigMigrationOptions {
1097
+ /** The codec being wrapped. */
1063
1098
  readonly codec: ConfigCodec;
1099
+ /** The migration steps; they run in ascending `version` order. */
1064
1100
  readonly migrations: ReadonlyArray<ConfigFileMigration>;
1101
+ /** How the version is read and written; defaults to {@link (VersionAccess:variable).default}, a top-level `version` field. */
1065
1102
  readonly versionAccess?: VersionAccess;
1066
1103
  }
1067
1104
  /** Versioned migration support for config codecs. @public */
@@ -1075,28 +1112,44 @@ export declare class ConfigMigration {
1075
1112
  * {@link ConfigMigrationError} rather than flattening migration failures into
1076
1113
  * the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
1077
1114
  * in its error type.
1115
+ *
1116
+ * @example
1117
+ * ```ts
1118
+ * import { ConfigMigration, JsonCodec } from "@effected/config-file";
1119
+ * import { Effect } from "effect";
1120
+ *
1121
+ * const codec = ConfigMigration.make({
1122
+ * codec: JsonCodec,
1123
+ * migrations: [
1124
+ * {
1125
+ * version: 2,
1126
+ * name: "rename-host",
1127
+ * up: (raw) => Effect.succeed({ ...(raw as object), host: (raw as { hostname?: string }).hostname }),
1128
+ * },
1129
+ * ],
1130
+ * });
1131
+ * ```
1078
1132
  */
1079
1133
  static readonly make: (options: ConfigMigrationOptions) => ConfigCodec<ConfigCodecError | ConfigMigrationError>;
1080
1134
  }
1081
1135
  //#endregion
1082
1136
  //#region src/ConfigProvider.d.ts
1083
1137
  /**
1084
- * Expose a loaded, merged, schema-validated document as a v4 `ConfigProvider`,
1138
+ * Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
1085
1139
  * so it can be read through `Config.String("port")` and layered beneath other
1086
1140
  * providers.
1087
1141
  *
1088
1142
  * @remarks
1089
1143
  * Strictly additive, and deliberately in its own module so it never becomes a
1090
1144
  * required import. The schema-validated whole-document
1091
- * {@link ConfigFileShape.load} remains the primary API: v4's `Config` has no
1145
+ * {@link ConfigFileShape.load} remains the primary API: `Config` has no
1092
1146
  * whole-document story, and this function is the bridge, not a replacement.
1093
1147
  *
1094
1148
  * Three properties, in order of how easy they are to lose:
1095
1149
  *
1096
1150
  * 1. **A missing file is a failure, not an empty provider.** The
1097
1151
  * {@link ConfigFileNotFoundError} propagates. Handing back an empty provider
1098
- * would silently turn every subsequent `Config` read into "not found", which
1099
- * is the exact class of lie this port exists to undo.
1152
+ * would silently turn every subsequent `Config` read into "not found".
1100
1153
  * 2. **Nested keys are structural, not dotted.** `fromUnknown` descends one path
1101
1154
  * segment at a time, so `{ db: { host } }` is reached with
1102
1155
  * `Config.nested(Config.String("host"), "db")` — never `Config.String("db.host")`,
@@ -1124,8 +1177,14 @@ export declare class ConfigMigration {
1124
1177
  *
1125
1178
  * @example
1126
1179
  * ```ts
1127
- * const fileProvider = yield* asConfigProvider(cfg);
1128
- * const provider = ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider);
1180
+ * import { asConfigProvider } from "@effected/config-file";
1181
+ * import { ConfigProvider, Effect } from "effect";
1182
+ *
1183
+ * const program = Effect.gen(function* () {
1184
+ * const cfg = yield* AppConfig; // a ConfigFile.Service class
1185
+ * const fileProvider = yield* asConfigProvider(cfg);
1186
+ * return ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider);
1187
+ * });
1129
1188
  * ```
1130
1189
  *
1131
1190
  * @public
@@ -1152,9 +1211,8 @@ interface LayerConfigProviderOptions {
1152
1211
  * `ConfigProvider`, so `Config` accessors read env first and the file second.
1153
1212
  *
1154
1213
  * @remarks
1155
- * This is the composition the v3 library could not express. This layer
1156
- * composes beneath the **ambient** `ConfigProvider`, v4's `Context.Reference`
1157
- * for it, so a consumer supplying `ConfigProvider.layer(...)` controls
1214
+ * This layer composes beneath the **ambient** `ConfigProvider` (a
1215
+ * `Context.Reference`), so a consumer supplying `ConfigProvider.layer(...)` controls
1158
1216
  * precedence explicitly: whatever that provider resolves wins, and the config
1159
1217
  * file supplies whatever it lacks. That reference's own default is
1160
1218
  * `ConfigProvider.fromEnv()`, which is what most applications want; this
@@ -1187,10 +1245,9 @@ declare const ConfigEncryptionError_base: Schema.Class<ConfigEncryptionError, Sc
1187
1245
  * failed.
1188
1246
  *
1189
1247
  * @remarks
1190
- * Its own error rather than v3's `"key-derivation"` value on the generic
1191
- * `ConfigCodecError.operation` union — an encryption-only concern was leaking
1192
- * into every codec's error type. `cause` preserves the underlying host failure
1193
- * structurally; v3 assembled it into a prose `reason` string.
1248
+ * Its own error rather than a value on the generic `ConfigCodecError.operation`
1249
+ * union, so an encryption-only concern does not leak into every codec's error
1250
+ * type. `cause` preserves the underlying host failure structurally.
1194
1251
  *
1195
1252
  * @public
1196
1253
  */
@@ -1260,6 +1317,14 @@ export declare const EncryptedCodecKey: {
1260
1317
  * flattening — the inner codec's failures stay distinguishable from
1261
1318
  * cryptographic ones.
1262
1319
  *
1320
+ * @example
1321
+ * ```ts
1322
+ * import { EncryptedCodec, EncryptedCodecKey, JsonCodec } from "@effected/config-file";
1323
+ *
1324
+ * const salt = new Uint8Array(16); // use a stored, random per-deployment salt
1325
+ * const codec = EncryptedCodec(JsonCodec, EncryptedCodecKey.fromPassphrase("correct horse", salt));
1326
+ * ```
1327
+ *
1263
1328
  * @public
1264
1329
  */
1265
1330
  export declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: EncryptedCodecKey): ConfigCodec<E | ConfigEncryptionError>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/config-file",
3
- "version": "0.13.0",
3
+ "version": "0.14.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,12 @@
43
43
  "./package.json": "./package.json"
44
44
  },
45
45
  "peerDependencies": {
46
- "@effected/jsonc": "^0.14.0",
47
- "@effected/toml": "^0.10.0",
48
- "@effected/walker": "^0.13.0",
49
- "@effected/yaml": "^0.18.0",
50
- "effect": "4.0.0-rc.118"
46
+ "@effected/glob": "^0.10.0",
47
+ "@effected/jsonc": "^0.15.0",
48
+ "@effected/toml": "^0.11.0",
49
+ "@effected/walker": "^0.15.0",
50
+ "@effected/yaml": "^0.19.0",
51
+ "effect": "^4.0.0"
51
52
  },
52
53
  "engines": {
53
54
  "node": ">=24.11.0"