@effected/config-file 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ConfigFile.js +101 -63
- package/ConfigMigration.js +13 -12
- package/ConfigResolver.js +28 -7
- package/README.md +20 -0
- package/index.d.ts +155 -19
- package/package.json +5 -5
package/ConfigFile.js
CHANGED
|
@@ -100,16 +100,6 @@ var ConfigValidationError = class extends Schema.TaggedErrorClass()("ConfigValid
|
|
|
100
100
|
})}`;
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
|
-
/**
|
|
104
|
-
* Create a uniquely-keyed service class for one config schema.
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* ```ts
|
|
108
|
-
* class AppConfig extends ConfigFile.Service<AppConfig, AppShape>()("app/Config") {}
|
|
109
|
-
* ```
|
|
110
|
-
*
|
|
111
|
-
* @public
|
|
112
|
-
*/
|
|
113
103
|
const Service = () => (id) => Context.Service()(id);
|
|
114
104
|
const makeImpl = (options, fs, path, resolverEnv) => {
|
|
115
105
|
/**
|
|
@@ -329,60 +319,12 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
329
319
|
update
|
|
330
320
|
};
|
|
331
321
|
};
|
|
332
|
-
/**
|
|
333
|
-
* Build the live layer for a config service class.
|
|
334
|
-
*
|
|
335
|
-
* @remarks
|
|
336
|
-
* Resolver requirements flow into the layer's `R` type. v3 cast them away with
|
|
337
|
-
* `as Effect.Effect<Option<string>>`, making `Layer<Service, never, FileSystem>`
|
|
338
|
-
* a claim rather than a proof.
|
|
339
|
-
*
|
|
340
|
-
* `ConfigFile.layer` is a layer-RETURNING function, not a layer: calling it
|
|
341
|
-
* twice builds two independent service instances. Bind its result to a const
|
|
342
|
-
* and provide that const, per the memoization discipline — do not call
|
|
343
|
-
* `ConfigFile.layer(...)` inline at each provide site.
|
|
344
|
-
*
|
|
345
|
-
* @public
|
|
346
|
-
*/
|
|
347
322
|
const layer = (tag, options) => Layer.effect(tag, Effect.gen(function* () {
|
|
348
323
|
const fs = yield* FileSystem.FileSystem;
|
|
349
324
|
const path = yield* Path.Path;
|
|
350
325
|
const resolverEnv = yield* Effect.context();
|
|
351
326
|
return makeImpl(options, fs, path, resolverEnv);
|
|
352
327
|
}));
|
|
353
|
-
/**
|
|
354
|
-
* A scoped layer that seeds `files` into a temp directory, wires the **real**
|
|
355
|
-
* live implementation over them, and removes the directory when the scope
|
|
356
|
-
* closes.
|
|
357
|
-
*
|
|
358
|
-
* @remarks
|
|
359
|
-
* Deliberately not a mock. It delegates to the very same `makeImpl` that
|
|
360
|
-
* {@link ConfigFile.layer} uses, so tests exercise the actual codec, resolver
|
|
361
|
-
* and merge pipeline rather than a parallel implementation that can drift from
|
|
362
|
-
* it. A stubbed test layer would make every downstream test a claim about the
|
|
363
|
-
* stub instead of about the code under test.
|
|
364
|
-
*
|
|
365
|
-
* Platform-agnostic: the consumer supplies the `FileSystem` layer, and the temp
|
|
366
|
-
* directory is created through `FileSystem.makeTempDirectory` rather than
|
|
367
|
-
* `node:fs`.
|
|
368
|
-
*
|
|
369
|
-
* `Layer.scoped` does not exist in v4. `Layer.effect` types its layer as
|
|
370
|
-
* `Layer<I, E, Exclude<R, Scope>>`, so an `Effect.addFinalizer` inside it binds
|
|
371
|
-
* to the layer's own scope and runs on release without surfacing `Scope` in the
|
|
372
|
-
* layer's requirements.
|
|
373
|
-
*
|
|
374
|
-
* @example
|
|
375
|
-
* ```ts
|
|
376
|
-
* const TestConfig = ConfigFile.testLayer(AppConfig, {
|
|
377
|
-
* schema: AppShape,
|
|
378
|
-
* codec: JsonCodec,
|
|
379
|
-
* strategy: MergeStrategy.firstMatch<AppShape>(),
|
|
380
|
-
* files: { ".apprc": `{"port":4242}` },
|
|
381
|
-
* }).pipe(Layer.provide(NodeServices.layer));
|
|
382
|
-
* ```
|
|
383
|
-
*
|
|
384
|
-
* @public
|
|
385
|
-
*/
|
|
386
328
|
const testLayer = (tag, options) => Layer.effect(tag, Effect.gen(function* () {
|
|
387
329
|
const fs = yield* FileSystem.FileSystem;
|
|
388
330
|
const path = yield* Path.Path;
|
|
@@ -406,15 +348,111 @@ const testLayer = (tag, options) => Layer.effect(tag, Effect.gen(function* () {
|
|
|
406
348
|
...options.validate !== void 0 && { validate: options.validate }
|
|
407
349
|
}, fs, path, resolverEnv);
|
|
408
350
|
}));
|
|
351
|
+
const read = (path, options) => Effect.gen(function* () {
|
|
352
|
+
const raw = yield* (yield* FileSystem.FileSystem).readFileString(path).pipe(Effect.mapError((cause) => new ConfigFileReadError({
|
|
353
|
+
path,
|
|
354
|
+
cause
|
|
355
|
+
})));
|
|
356
|
+
const parsed = yield* options.codec.parse(raw);
|
|
357
|
+
return yield* Schema.decodeUnknownEffect(options.schema)(parsed).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
|
|
358
|
+
path: Option.some(path),
|
|
359
|
+
issue: error.issue
|
|
360
|
+
}))));
|
|
361
|
+
}).pipe(Effect.withSpan("ConfigFile.read", { attributes: { path } }));
|
|
409
362
|
/**
|
|
410
|
-
* The config file service: a per-schema service factory
|
|
363
|
+
* The config file service: a per-schema service factory, its layers, and the
|
|
364
|
+
* one-shot {@link ConfigFile.read}.
|
|
411
365
|
*
|
|
412
366
|
* @public
|
|
413
367
|
*/
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
368
|
+
var ConfigFile = class {
|
|
369
|
+
constructor() {}
|
|
370
|
+
/**
|
|
371
|
+
* Create a uniquely-keyed service class for one config schema.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```ts
|
|
375
|
+
* class AppConfig extends ConfigFile.Service<AppConfig, AppShape>()("app/Config") {}
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
static Service = Service;
|
|
379
|
+
/**
|
|
380
|
+
* Build the live layer for a config service class.
|
|
381
|
+
*
|
|
382
|
+
* @remarks
|
|
383
|
+
* Resolver requirements flow into the layer's `R` type. v3 cast them away with
|
|
384
|
+
* `as Effect.Effect<Option<string>>`, making `Layer<Service, never, FileSystem>`
|
|
385
|
+
* a claim rather than a proof.
|
|
386
|
+
*
|
|
387
|
+
* `ConfigFile.layer` is a layer-RETURNING function, not a layer: calling it
|
|
388
|
+
* twice builds two independent service instances. Bind its result to a const
|
|
389
|
+
* and provide that const, per the memoization discipline — do not call
|
|
390
|
+
* `ConfigFile.layer(...)` inline at each provide site.
|
|
391
|
+
*/
|
|
392
|
+
static layer = layer;
|
|
393
|
+
/**
|
|
394
|
+
* A scoped layer that seeds `files` into a temp directory, wires the **real**
|
|
395
|
+
* live implementation over them, and removes the directory when the scope
|
|
396
|
+
* closes.
|
|
397
|
+
*
|
|
398
|
+
* @remarks
|
|
399
|
+
* Deliberately not a mock. It delegates to the very same `makeImpl` that
|
|
400
|
+
* {@link ConfigFile.layer} uses, so tests exercise the actual codec, resolver
|
|
401
|
+
* and merge pipeline rather than a parallel implementation that can drift from
|
|
402
|
+
* it. A stubbed test layer would make every downstream test a claim about the
|
|
403
|
+
* stub instead of about the code under test.
|
|
404
|
+
*
|
|
405
|
+
* Platform-agnostic: the consumer supplies the `FileSystem` layer, and the temp
|
|
406
|
+
* directory is created through `FileSystem.makeTempDirectory` rather than
|
|
407
|
+
* `node:fs`.
|
|
408
|
+
*
|
|
409
|
+
* `Layer.scoped` does not exist in v4. `Layer.effect` types its layer as
|
|
410
|
+
* `Layer<I, E, Exclude<R, Scope>>`, so an `Effect.addFinalizer` inside it binds
|
|
411
|
+
* to the layer's own scope and runs on release without surfacing `Scope` in the
|
|
412
|
+
* layer's requirements.
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```ts
|
|
416
|
+
* const TestConfig = ConfigFile.testLayer(AppConfig, {
|
|
417
|
+
* schema: AppShape,
|
|
418
|
+
* codec: JsonCodec,
|
|
419
|
+
* strategy: MergeStrategy.firstMatch<AppShape>(),
|
|
420
|
+
* files: { ".apprc": `{"port":4242}` },
|
|
421
|
+
* }).pipe(Layer.provide(NodeServices.layer));
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
static testLayer = testLayer;
|
|
425
|
+
/**
|
|
426
|
+
* Read, decode and validate one explicit path — no service, no layer, no tag.
|
|
427
|
+
*
|
|
428
|
+
* @remarks
|
|
429
|
+
* The one-shot form. {@link ConfigFile.layer} binds schema and codec at layer
|
|
430
|
+
* construction, which is the right model for a config file an application
|
|
431
|
+
* *has* — several candidate locations, `save`/`update`, migrations, events —
|
|
432
|
+
* and heavy for a call site that decodes one known path once, where it costs a
|
|
433
|
+
* service subclass, a layer bound to a const and a provide at the boundary.
|
|
434
|
+
* Unlike the service, `read` takes its schema per call, so one call site can
|
|
435
|
+
* read several unrelated files without a service class each.
|
|
436
|
+
*
|
|
437
|
+
* It is deliberately read-only and discovery-free: there is no resolver chain
|
|
438
|
+
* and no write path. Reach for {@link ConfigFile.layer} the moment either is
|
|
439
|
+
* wanted, rather than growing this.
|
|
440
|
+
*
|
|
441
|
+
* The error channel is `ConfigReadError` — the same narrowed union
|
|
442
|
+
* {@link ConfigFileShape.loadFrom} carries, with causes and schema issues held
|
|
443
|
+
* structurally rather than flattened into a message.
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```ts
|
|
447
|
+
* import { ConfigFile, JsonCodec } from "@effected/config-file";
|
|
448
|
+
*
|
|
449
|
+
* const config = yield* ConfigFile.read(inputs.configFile, {
|
|
450
|
+
* schema: MyConfig,
|
|
451
|
+
* codec: JsonCodec,
|
|
452
|
+
* });
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
static read = read;
|
|
418
456
|
};
|
|
419
457
|
|
|
420
458
|
//#endregion
|
package/ConfigMigration.js
CHANGED
|
@@ -61,17 +61,6 @@ const runPhase = (phase, version, name, run) => Effect.suspend(run).pipe(Effect.
|
|
|
61
61
|
phase,
|
|
62
62
|
cause
|
|
63
63
|
})));
|
|
64
|
-
/**
|
|
65
|
-
* Wrap a codec so that parsed content is brought up to the latest version.
|
|
66
|
-
*
|
|
67
|
-
* @remarks
|
|
68
|
-
* The returned codec's error channel **widens** to include
|
|
69
|
-
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
70
|
-
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
71
|
-
* in its error type.
|
|
72
|
-
*
|
|
73
|
-
* @public
|
|
74
|
-
*/
|
|
75
64
|
const make = (options) => {
|
|
76
65
|
const access = options.versionAccess ?? VersionAccess.default;
|
|
77
66
|
const sorted = [...options.migrations].sort((a, b) => a.version - b.version);
|
|
@@ -91,7 +80,19 @@ const make = (options) => {
|
|
|
91
80
|
};
|
|
92
81
|
};
|
|
93
82
|
/** Versioned migration support for config codecs. @public */
|
|
94
|
-
|
|
83
|
+
var ConfigMigration = class {
|
|
84
|
+
constructor() {}
|
|
85
|
+
/**
|
|
86
|
+
* Wrap a codec so that parsed content is brought up to the latest version.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* The returned codec's error channel **widens** to include
|
|
90
|
+
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
91
|
+
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
92
|
+
* in its error type.
|
|
93
|
+
*/
|
|
94
|
+
static make = make;
|
|
95
|
+
};
|
|
95
96
|
|
|
96
97
|
//#endregion
|
|
97
98
|
export { ConfigMigration, ConfigMigrationError, VersionAccess };
|
package/ConfigResolver.js
CHANGED
|
@@ -82,13 +82,34 @@ const systemEtc = (options) => ({
|
|
|
82
82
|
*
|
|
83
83
|
* @public
|
|
84
84
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
var ConfigResolver = class {
|
|
86
|
+
constructor() {}
|
|
87
|
+
/** Resolves to `target` when it exists on disk, or `Option.none()` when it does not — no walking, no filename convention. */
|
|
88
|
+
static explicitPath = explicitPath;
|
|
89
|
+
/** Resolves `path.join(dir, filename)` when it exists on disk, or `Option.none()` when it does not. */
|
|
90
|
+
static staticDir = staticDir;
|
|
91
|
+
/**
|
|
92
|
+
* Ascends from `cwd` (or the process cwd) toward `stopAt`, resolving the
|
|
93
|
+
* first `subpaths/filename` combination found at each level.
|
|
94
|
+
*/
|
|
95
|
+
static upwardWalk = upwardWalk;
|
|
96
|
+
/**
|
|
97
|
+
* Ascends from `cwd` to the nearest workspace root — a directory with a
|
|
98
|
+
* `pnpm-workspace.yaml`, or a `package.json` carrying a `workspaces` field —
|
|
99
|
+
* then resolves the first `subpaths/filename` combination found under it.
|
|
100
|
+
*/
|
|
101
|
+
static workspaceRoot = workspaceRoot;
|
|
102
|
+
/**
|
|
103
|
+
* Ascends from `cwd` to the nearest git root — a directory containing a
|
|
104
|
+
* `.git` entry, directory or file (the latter for a worktree) — then
|
|
105
|
+
* resolves the first `subpaths/filename` combination found under it.
|
|
106
|
+
*/
|
|
107
|
+
static gitRoot = gitRoot;
|
|
108
|
+
/**
|
|
109
|
+
* Resolves `<dir>/<app>/<filename>` under the system config root (`/etc` by
|
|
110
|
+
* default). Always `Option.none()` on Windows, where `/etc` has no meaning.
|
|
111
|
+
*/
|
|
112
|
+
static systemEtc = systemEtc;
|
|
92
113
|
};
|
|
93
114
|
|
|
94
115
|
//#endregion
|
package/README.md
CHANGED
|
@@ -98,6 +98,25 @@ export const SettingsLive = ConfigFile.layer(SettingsConfig, {
|
|
|
98
98
|
});
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
## Reading one known path
|
|
102
|
+
|
|
103
|
+
Not every caller has a config file — some just have one path a caller already vouched for, such as a CLI's `--config` flag. `ConfigFile.read` is the one-shot escape from the service, the layer and the resolver chain: read, decode and validate a single path, with the schema and codec named per call rather than bound to a service class:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { ConfigFile, JsonCodec } from "@effected/config-file";
|
|
107
|
+
import { NodeFileSystem } from "@effect/platform-node";
|
|
108
|
+
import { Effect, Schema } from "effect";
|
|
109
|
+
|
|
110
|
+
class MyConfig extends Schema.Class<MyConfig>("MyConfig")({ port: Schema.Number }) {}
|
|
111
|
+
|
|
112
|
+
const program = ConfigFile.read("./app.config.json", { schema: MyConfig, codec: JsonCodec });
|
|
113
|
+
|
|
114
|
+
Effect.runPromise(program.pipe(Effect.provide(NodeFileSystem.layer))).then(console.log);
|
|
115
|
+
// MyConfig { port: 3000 }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It is deliberately read-only and discovery-free — no resolver chain, no `save`/`update`. Reach for `ConfigFile.layer` the moment either is wanted.
|
|
119
|
+
|
|
101
120
|
## Errors
|
|
102
121
|
|
|
103
122
|
Every failure is a tagged error you route on with `Effect.catchTag`. The tags exist so that recovery can differ:
|
|
@@ -166,6 +185,7 @@ export const secret = EncryptedCodec(migrating, EncryptedCodecKey.fromPassphrase
|
|
|
166
185
|
## Features
|
|
167
186
|
|
|
168
187
|
- `ConfigFile.Service` / `ConfigFile.layer` / `ConfigFile.testLayer` — a per-schema service class and its layers. `testLayer` seeds files into a temp directory and wires the *real* implementation over them, so tests exercise the actual pipeline rather than a stub that can drift from it.
|
|
188
|
+
- `ConfigFile.read` — the one-shot escape from the service: read, decode and validate one explicit path, schema and codec named per call, with no resolver chain and no write path.
|
|
169
189
|
- `ConfigResolver` — `explicitPath`, `staticDir`, `upwardWalk`, `workspaceRoot`, `gitRoot` and `systemEtc`. A resolver's error channel is `never` by contract: every filesystem failure becomes `Option.none()`, so one unreadable tier never aborts the chain.
|
|
170
190
|
- `MergeStrategy` — `firstMatch` and `layeredMerge`, combining discovered sources in priority order.
|
|
171
191
|
- `JsonCodec`, `JsoncCodec`, `YamlCodec`, `TomlCodec` — JSON, JSONC, YAML and TOML in the box, exported free-standing so an unused format's engine is tree-shaken away.
|
package/index.d.ts
CHANGED
|
@@ -248,29 +248,50 @@ interface ConfigResolver<R = never> {
|
|
|
248
248
|
*
|
|
249
249
|
* @public
|
|
250
250
|
*/
|
|
251
|
-
declare
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
declare class ConfigResolver {
|
|
252
|
+
private constructor();
|
|
253
|
+
/** Resolves to `target` when it exists on disk, or `Option.none()` when it does not — no walking, no filename convention. */
|
|
254
|
+
static readonly explicitPath: (target: string) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
255
|
+
/** Resolves `path.join(dir, filename)` when it exists on disk, or `Option.none()` when it does not. */
|
|
256
|
+
static readonly staticDir: (options: {
|
|
254
257
|
readonly dir: string;
|
|
255
258
|
readonly filename: string;
|
|
256
259
|
}) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
257
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Ascends from `cwd` (or the process cwd) toward `stopAt`, resolving the
|
|
262
|
+
* first `subpaths/filename` combination found at each level.
|
|
263
|
+
*/
|
|
264
|
+
static readonly upwardWalk: (options: {
|
|
258
265
|
readonly filename: string;
|
|
259
266
|
readonly cwd?: string;
|
|
260
267
|
readonly stopAt?: string;
|
|
261
268
|
readonly subpaths?: ReadonlyArray<string>;
|
|
262
269
|
}) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
263
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Ascends from `cwd` to the nearest workspace root — a directory with a
|
|
272
|
+
* `pnpm-workspace.yaml`, or a `package.json` carrying a `workspaces` field —
|
|
273
|
+
* then resolves the first `subpaths/filename` combination found under it.
|
|
274
|
+
*/
|
|
275
|
+
static readonly workspaceRoot: (options: {
|
|
264
276
|
readonly filename: string;
|
|
265
277
|
readonly cwd?: string;
|
|
266
278
|
readonly subpaths?: ReadonlyArray<string>;
|
|
267
279
|
}) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
268
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Ascends from `cwd` to the nearest git root — a directory containing a
|
|
282
|
+
* `.git` entry, directory or file (the latter for a worktree) — then
|
|
283
|
+
* resolves the first `subpaths/filename` combination found under it.
|
|
284
|
+
*/
|
|
285
|
+
static readonly gitRoot: (options: {
|
|
269
286
|
readonly filename: string;
|
|
270
287
|
readonly cwd?: string;
|
|
271
288
|
readonly subpaths?: ReadonlyArray<string>;
|
|
272
289
|
}) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
273
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Resolves `<dir>/<app>/<filename>` under the system config root (`/etc` by
|
|
292
|
+
* default). Always `Option.none()` on Windows, where `/etc` has no meaning.
|
|
293
|
+
*/
|
|
294
|
+
static readonly systemEtc: (options: {
|
|
274
295
|
readonly app: string;
|
|
275
296
|
readonly filename: string;
|
|
276
297
|
/**
|
|
@@ -280,7 +301,7 @@ declare const ConfigResolver: {
|
|
|
280
301
|
*/
|
|
281
302
|
readonly dir?: string;
|
|
282
303
|
}) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
283
|
-
}
|
|
304
|
+
}
|
|
284
305
|
//#endregion
|
|
285
306
|
//#region src/MergeStrategy.d.ts
|
|
286
307
|
/**
|
|
@@ -571,7 +592,7 @@ interface ConfigFileOptions<A, I, RR> {
|
|
|
571
592
|
*
|
|
572
593
|
* @remarks
|
|
573
594
|
* Deliberately has no `resolvers`: `testLayer` synthesizes one
|
|
574
|
-
* {@link (ConfigResolver:
|
|
595
|
+
* {@link (ConfigResolver:class).staticDir} per seeded file, in `files`
|
|
575
596
|
* insertion order, so the first key wins under
|
|
576
597
|
* {@link (MergeStrategy:variable).firstMatch}.
|
|
577
598
|
*
|
|
@@ -601,15 +622,120 @@ interface ConfigFileTestOptions<A, I> {
|
|
|
601
622
|
readonly validate?: (value: A) => Effect.Effect<A, ConfigValidationError>;
|
|
602
623
|
}
|
|
603
624
|
/**
|
|
604
|
-
*
|
|
625
|
+
* Options for {@link ConfigFile.read}.
|
|
605
626
|
*
|
|
606
627
|
* @public
|
|
607
628
|
*/
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
readonly
|
|
611
|
-
|
|
612
|
-
|
|
629
|
+
interface ConfigReadOptions<A, I> {
|
|
630
|
+
/** The schema the document is decoded through. */
|
|
631
|
+
readonly schema: Schema.Codec<A, I>;
|
|
632
|
+
/**
|
|
633
|
+
* How file content becomes an unknown document.
|
|
634
|
+
*
|
|
635
|
+
* @remarks
|
|
636
|
+
* An explicit argument, never inferred from the file extension or defaulted
|
|
637
|
+
* to JSON. Naming the codec at the call site is what keeps the
|
|
638
|
+
* free-standing-codec tree-shaking guarantee: a consumer that only ever
|
|
639
|
+
* passes `JsonCodec` never references the JSONC, YAML or TOML modules, so
|
|
640
|
+
* their engines stay out of the bundle.
|
|
641
|
+
*/
|
|
642
|
+
readonly codec: ConfigCodec;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* The config file service: a per-schema service factory, its layers, and the
|
|
646
|
+
* one-shot {@link ConfigFile.read}.
|
|
647
|
+
*
|
|
648
|
+
* @public
|
|
649
|
+
*/
|
|
650
|
+
declare class ConfigFile {
|
|
651
|
+
private constructor();
|
|
652
|
+
/**
|
|
653
|
+
* Create a uniquely-keyed service class for one config schema.
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```ts
|
|
657
|
+
* class AppConfig extends ConfigFile.Service<AppConfig, AppShape>()("app/Config") {}
|
|
658
|
+
* ```
|
|
659
|
+
*/
|
|
660
|
+
static readonly Service: <Self, A>() => <const Id extends string>(id: Id) => Context.ServiceClass<Self, Id, ConfigFileShape<A>>;
|
|
661
|
+
/**
|
|
662
|
+
* Build the live layer for a config service class.
|
|
663
|
+
*
|
|
664
|
+
* @remarks
|
|
665
|
+
* Resolver requirements flow into the layer's `R` type. v3 cast them away with
|
|
666
|
+
* `as Effect.Effect<Option<string>>`, making `Layer<Service, never, FileSystem>`
|
|
667
|
+
* a claim rather than a proof.
|
|
668
|
+
*
|
|
669
|
+
* `ConfigFile.layer` is a layer-RETURNING function, not a layer: calling it
|
|
670
|
+
* twice builds two independent service instances. Bind its result to a const
|
|
671
|
+
* and provide that const, per the memoization discipline — do not call
|
|
672
|
+
* `ConfigFile.layer(...)` inline at each provide site.
|
|
673
|
+
*/
|
|
674
|
+
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>;
|
|
675
|
+
/**
|
|
676
|
+
* A scoped layer that seeds `files` into a temp directory, wires the **real**
|
|
677
|
+
* live implementation over them, and removes the directory when the scope
|
|
678
|
+
* closes.
|
|
679
|
+
*
|
|
680
|
+
* @remarks
|
|
681
|
+
* Deliberately not a mock. It delegates to the very same `makeImpl` that
|
|
682
|
+
* {@link ConfigFile.layer} uses, so tests exercise the actual codec, resolver
|
|
683
|
+
* and merge pipeline rather than a parallel implementation that can drift from
|
|
684
|
+
* it. A stubbed test layer would make every downstream test a claim about the
|
|
685
|
+
* stub instead of about the code under test.
|
|
686
|
+
*
|
|
687
|
+
* Platform-agnostic: the consumer supplies the `FileSystem` layer, and the temp
|
|
688
|
+
* directory is created through `FileSystem.makeTempDirectory` rather than
|
|
689
|
+
* `node:fs`.
|
|
690
|
+
*
|
|
691
|
+
* `Layer.scoped` does not exist in v4. `Layer.effect` types its layer as
|
|
692
|
+
* `Layer<I, E, Exclude<R, Scope>>`, so an `Effect.addFinalizer` inside it binds
|
|
693
|
+
* to the layer's own scope and runs on release without surfacing `Scope` in the
|
|
694
|
+
* layer's requirements.
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```ts
|
|
698
|
+
* const TestConfig = ConfigFile.testLayer(AppConfig, {
|
|
699
|
+
* schema: AppShape,
|
|
700
|
+
* codec: JsonCodec,
|
|
701
|
+
* strategy: MergeStrategy.firstMatch<AppShape>(),
|
|
702
|
+
* files: { ".apprc": `{"port":4242}` },
|
|
703
|
+
* }).pipe(Layer.provide(NodeServices.layer));
|
|
704
|
+
* ```
|
|
705
|
+
*/
|
|
706
|
+
static readonly testLayer: <Self, A, I>(tag: Context.Key<Self, ConfigFileShape<A>>, options: ConfigFileTestOptions<A, I>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path>;
|
|
707
|
+
/**
|
|
708
|
+
* Read, decode and validate one explicit path — no service, no layer, no tag.
|
|
709
|
+
*
|
|
710
|
+
* @remarks
|
|
711
|
+
* The one-shot form. {@link ConfigFile.layer} binds schema and codec at layer
|
|
712
|
+
* construction, which is the right model for a config file an application
|
|
713
|
+
* *has* — several candidate locations, `save`/`update`, migrations, events —
|
|
714
|
+
* and heavy for a call site that decodes one known path once, where it costs a
|
|
715
|
+
* service subclass, a layer bound to a const and a provide at the boundary.
|
|
716
|
+
* Unlike the service, `read` takes its schema per call, so one call site can
|
|
717
|
+
* read several unrelated files without a service class each.
|
|
718
|
+
*
|
|
719
|
+
* It is deliberately read-only and discovery-free: there is no resolver chain
|
|
720
|
+
* and no write path. Reach for {@link ConfigFile.layer} the moment either is
|
|
721
|
+
* wanted, rather than growing this.
|
|
722
|
+
*
|
|
723
|
+
* The error channel is `ConfigReadError` — the same narrowed union
|
|
724
|
+
* {@link ConfigFileShape.loadFrom} carries, with causes and schema issues held
|
|
725
|
+
* structurally rather than flattened into a message.
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* ```ts
|
|
729
|
+
* import { ConfigFile, JsonCodec } from "@effected/config-file";
|
|
730
|
+
*
|
|
731
|
+
* const config = yield* ConfigFile.read(inputs.configFile, {
|
|
732
|
+
* schema: MyConfig,
|
|
733
|
+
* codec: JsonCodec,
|
|
734
|
+
* });
|
|
735
|
+
* ```
|
|
736
|
+
*/
|
|
737
|
+
static readonly read: <A, I>(path: string, options: ConfigReadOptions<A, I>) => Effect.Effect<A, ConfigReadError, FileSystem.FileSystem>;
|
|
738
|
+
}
|
|
613
739
|
//#endregion
|
|
614
740
|
//#region src/ConfigMigration.d.ts
|
|
615
741
|
declare const ConfigMigrationError_base: Schema.Class<ConfigMigrationError, Schema.TaggedStruct<"ConfigMigrationError", {
|
|
@@ -670,9 +796,19 @@ interface ConfigMigrationOptions {
|
|
|
670
796
|
readonly versionAccess?: VersionAccess;
|
|
671
797
|
}
|
|
672
798
|
/** Versioned migration support for config codecs. @public */
|
|
673
|
-
declare
|
|
674
|
-
|
|
675
|
-
|
|
799
|
+
declare class ConfigMigration {
|
|
800
|
+
private constructor();
|
|
801
|
+
/**
|
|
802
|
+
* Wrap a codec so that parsed content is brought up to the latest version.
|
|
803
|
+
*
|
|
804
|
+
* @remarks
|
|
805
|
+
* The returned codec's error channel **widens** to include
|
|
806
|
+
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
807
|
+
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
808
|
+
* in its error type.
|
|
809
|
+
*/
|
|
810
|
+
static readonly make: (options: ConfigMigrationOptions) => ConfigCodec<ConfigCodecError | ConfigMigrationError>;
|
|
811
|
+
}
|
|
676
812
|
//#endregion
|
|
677
813
|
//#region src/ConfigProvider.d.ts
|
|
678
814
|
/**
|
|
@@ -928,5 +1064,5 @@ declare const TomlCodec: ConfigCodec;
|
|
|
928
1064
|
*/
|
|
929
1065
|
declare const YamlCodec: ConfigCodec;
|
|
930
1066
|
//#endregion
|
|
931
|
-
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, 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 };
|
|
1067
|
+
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 };
|
|
932
1068
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/config-file",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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,10 +43,10 @@
|
|
|
43
43
|
"./package.json": "./package.json"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@effected/jsonc": "
|
|
47
|
-
"@effected/toml": "
|
|
48
|
-
"@effected/walker": "
|
|
49
|
-
"@effected/yaml": "
|
|
46
|
+
"@effected/jsonc": "^0.5.2",
|
|
47
|
+
"@effected/toml": "^0.3.2",
|
|
48
|
+
"@effected/walker": "^0.3.4",
|
|
49
|
+
"@effected/yaml": "^0.6.1",
|
|
50
50
|
"effect": "4.0.0-beta.101"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|