@effected/config-file 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ConfigFile.js +38 -8
- package/README.md +2 -1
- package/index.d.ts +47 -3
- package/package.json +3 -3
package/ConfigFile.js
CHANGED
|
@@ -120,6 +120,13 @@ const withCodecPath = (error, target) => error instanceof ConfigCodecError && er
|
|
|
120
120
|
cause: error.cause,
|
|
121
121
|
path: target
|
|
122
122
|
}) : error;
|
|
123
|
+
/**
|
|
124
|
+
* Put `header` in front of `document`, separated by exactly one newline.
|
|
125
|
+
*
|
|
126
|
+
* A header that already ends in a newline is not given another — a caller who
|
|
127
|
+
* built the line with a trailing `"\n"` and one who did not get the same file.
|
|
128
|
+
*/
|
|
129
|
+
const prependHeader = (document, header) => header === void 0 ? document : header.endsWith("\n") ? `${header}${document}` : `${header}\n${document}`;
|
|
123
130
|
const makeImpl = (options, fs, path, resolverEnv) => {
|
|
124
131
|
/**
|
|
125
132
|
* Publish one event, or nothing at all.
|
|
@@ -252,24 +259,46 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
252
259
|
return yield* runValidate(decoded);
|
|
253
260
|
});
|
|
254
261
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
262
|
+
* Schema-encode, stringify, and prepend the header. Shared by `encode`,
|
|
263
|
+
* `write` and `save`. Not an `Effect.fn`: it is internal, and the public
|
|
264
|
+
* boundaries that call it already open a span.
|
|
265
|
+
*
|
|
266
|
+
* `target` is the file this text is destined for, when there is one. It is
|
|
267
|
+
* only ever used to NAME the file in an error — `encode` passes `none` and
|
|
268
|
+
* its errors honestly carry no path.
|
|
257
269
|
*/
|
|
258
|
-
const
|
|
270
|
+
const encodeTo = (value, target, encodeOptions) => Effect.gen(function* () {
|
|
259
271
|
const encoded = yield* Schema.encodeEffect(options.schema)(value).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
|
|
260
|
-
path:
|
|
272
|
+
path: target,
|
|
261
273
|
issue: error.issue
|
|
262
274
|
}))));
|
|
263
|
-
const serialized = yield* options.codec.stringify(encoded).pipe(Effect.mapError((error) =>
|
|
275
|
+
const serialized = yield* options.codec.stringify(encoded).pipe(Effect.mapError((error) => Option.match(target, {
|
|
276
|
+
onNone: () => error,
|
|
277
|
+
onSome: (file) => withCodecPath(error, file)
|
|
278
|
+
})));
|
|
279
|
+
return prependHeader(serialized, encodeOptions?.header);
|
|
280
|
+
});
|
|
281
|
+
/**
|
|
282
|
+
* `encodeTo` a known file, then write it. Shared by `write` and `save`.
|
|
283
|
+
*
|
|
284
|
+
* `StringifyFailed` is emitted HERE, not in `encodeTo`: it is a write-path
|
|
285
|
+
* event, and `encode` promises to publish nothing. A subscriber counting
|
|
286
|
+
* failed writes must not see a dry run's codec failure.
|
|
287
|
+
*/
|
|
288
|
+
const encodeAndWrite = (value, target, encodeOptions) => Effect.gen(function* () {
|
|
289
|
+
const serialized = yield* encodeTo(value, Option.some(target), encodeOptions).pipe(Effect.tapError((error) => error._tag === "ConfigCodecError" ? emit({
|
|
264
290
|
_tag: "StringifyFailed",
|
|
265
291
|
codec: options.codec.name,
|
|
266
292
|
error
|
|
267
|
-
})));
|
|
293
|
+
}) : Effect.void));
|
|
268
294
|
yield* fs.writeFileString(target, serialized).pipe(Effect.mapError((cause) => new ConfigFileWriteError({
|
|
269
295
|
path: target,
|
|
270
296
|
cause
|
|
271
297
|
})));
|
|
272
298
|
});
|
|
299
|
+
const encode = Effect.fn("ConfigFile.encode")(function* (value, encodeOptions) {
|
|
300
|
+
return yield* encodeTo(value, Option.none(), encodeOptions);
|
|
301
|
+
});
|
|
273
302
|
/**
|
|
274
303
|
* Resolve `defaultPath`, `mkdir -p` its parent and write. Shared by `save`
|
|
275
304
|
* and `update`, and — crucially — emits nothing.
|
|
@@ -290,8 +319,8 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
290
319
|
yield* encodeAndWrite(value, target);
|
|
291
320
|
return target;
|
|
292
321
|
});
|
|
293
|
-
const write = Effect.fn("ConfigFile.write")(function* (value, target) {
|
|
294
|
-
yield* encodeAndWrite(value, target);
|
|
322
|
+
const write = Effect.fn("ConfigFile.write")(function* (value, target, encodeOptions) {
|
|
323
|
+
yield* encodeAndWrite(value, target, encodeOptions);
|
|
295
324
|
yield* emit({
|
|
296
325
|
_tag: "Written",
|
|
297
326
|
path: target
|
|
@@ -335,6 +364,7 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
335
364
|
discover: discover(),
|
|
336
365
|
loadOrDefault,
|
|
337
366
|
validate,
|
|
367
|
+
encode,
|
|
338
368
|
write,
|
|
339
369
|
save,
|
|
340
370
|
update
|
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ Every failure is a tagged error you route on with `Effect.catchTag`. The tags ex
|
|
|
158
158
|
| `ConfigMigrationError` | A versioned migration failed. Carries `version`, `name`, `phase` and the structural `cause`. | Report which step failed; the config on disk is left untouched. |
|
|
159
159
|
| `ConfigEncryptionError` | An encrypt, decrypt, key-derivation or base64 step failed. Carries `phase` and the structural `cause`. | A wrong passphrase and a corrupt envelope both land here; inspect `phase`. |
|
|
160
160
|
|
|
161
|
-
`ConfigLoadError`, `ConfigReadError`, `ConfigWriteError`, `ConfigSaveError` and `ConfigUpdateError` are exported unions naming exactly the failures each method can produce. Catching one tag narrows the union, leaving the rest to propagate:
|
|
161
|
+
`ConfigLoadError`, `ConfigReadError`, `ConfigEncodeError`, `ConfigWriteError`, `ConfigSaveError` and `ConfigUpdateError` are exported unions naming exactly the failures each method can produce — `ConfigEncodeError` is `ConfigWriteError` minus `ConfigFileWriteError`, because `encode` never touches the disk. Catching one tag narrows the union, leaving the rest to propagate:
|
|
162
162
|
|
|
163
163
|
```ts
|
|
164
164
|
import type { ConfigFileShape } from "@effected/config-file";
|
|
@@ -213,6 +213,7 @@ export const secret = EncryptedCodec(migrating, EncryptedCodecKey.fromPassphrase
|
|
|
213
213
|
- `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.
|
|
214
214
|
- `parseOptions` — decode options threaded into every decode, on the layer and on `read`. `onExcessProperty: "error"` is the only way to report a typo'd section or a field the schema deliberately removed.
|
|
215
215
|
- `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.
|
|
216
|
+
- `encode` / `write` / `save` / `update` — the write path. `encode(value, options?)` returns the serialized text a `write` would put on disk without writing, emitting an event or naming a path in its errors: the primitive behind a `--dry-run` flag. `write` takes an explicit path the caller vouches for; `save` and `update` resolve the layer's `defaultPath` and create its parent directory. `encode` and `write` accept `{ header }`, text prepended verbatim above the document with exactly one newline between — a `#:schema https://…` directive atop a TOML file, say. The caller owns its validity in the target format; JSON has no comment syntax, so a header on `JsonCodec` yields an unparseable file by construction.
|
|
216
217
|
- `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.
|
|
217
218
|
- `MergeStrategy` — `firstMatch` and `layeredMerge`, combining discovered sources in priority order.
|
|
218
219
|
- `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
|
@@ -600,6 +600,38 @@ type ConfigReadError = ConfigFileReadError | ConfigCodecError | ConfigValidation
|
|
|
600
600
|
* @public
|
|
601
601
|
*/
|
|
602
602
|
type ConfigWriteError = ConfigFileWriteError | ConfigCodecError | ConfigValidationError;
|
|
603
|
+
/**
|
|
604
|
+
* The failure modes of {@link ConfigFileShape.encode}: everything on the write
|
|
605
|
+
* path except the write itself.
|
|
606
|
+
*
|
|
607
|
+
* @remarks
|
|
608
|
+
* Deliberately excludes {@link ConfigFileWriteError} — nothing touches the
|
|
609
|
+
* filesystem — so a `--dry-run` caller's error channel is honest about that.
|
|
610
|
+
*
|
|
611
|
+
* @public
|
|
612
|
+
*/
|
|
613
|
+
type ConfigEncodeError = ConfigCodecError | ConfigValidationError;
|
|
614
|
+
/**
|
|
615
|
+
* Options shared by {@link ConfigFileShape.encode} and {@link ConfigFileShape.write}.
|
|
616
|
+
*
|
|
617
|
+
* @public
|
|
618
|
+
*/
|
|
619
|
+
interface ConfigEncodeOptions {
|
|
620
|
+
/**
|
|
621
|
+
* Text emitted **verbatim** in front of the serialized document, separated
|
|
622
|
+
* from it by exactly one newline. If `header` already ends in `"\n"`, no
|
|
623
|
+
* further newline is added.
|
|
624
|
+
*
|
|
625
|
+
* @remarks
|
|
626
|
+
* The caller owns the header's validity in the target format: `#` comment
|
|
627
|
+
* lines for TOML and YAML, `//` for JSONC. JSON has no comment syntax, so a
|
|
628
|
+
* header on a JSON codec yields an unparseable file by construction — the
|
|
629
|
+
* service does not check. The motivating case is a schema directive such as
|
|
630
|
+
* `#:schema https://example.com/config.schema.json` at the top of a TOML
|
|
631
|
+
* file, which editors read for completion and validation.
|
|
632
|
+
*/
|
|
633
|
+
readonly header?: string;
|
|
634
|
+
}
|
|
603
635
|
/**
|
|
604
636
|
* The failure modes of {@link ConfigFileShape.save}.
|
|
605
637
|
*
|
|
@@ -646,15 +678,27 @@ interface ConfigFileShape<A> {
|
|
|
646
678
|
readonly loadOrDefault: (defaultValue: A) => Effect.Effect<A, ConfigReadError>;
|
|
647
679
|
/** Decode and validate an in-memory value. */
|
|
648
680
|
readonly validate: (value: unknown) => Effect.Effect<A, ConfigValidationError>;
|
|
681
|
+
/**
|
|
682
|
+
* Encode `value` to its serialized text without writing anything.
|
|
683
|
+
*
|
|
684
|
+
* @remarks
|
|
685
|
+
* Produces byte-for-byte what `write(value, path, options)` puts on disk —
|
|
686
|
+
* the primitive for `--dry-run` and "show me the file" callers. Emits no
|
|
687
|
+
* event, because nothing was written. A codec failure here carries no
|
|
688
|
+
* `path`, and a validation failure's `path` is `Option.none()`: there is no
|
|
689
|
+
* file to name.
|
|
690
|
+
*/
|
|
691
|
+
readonly encode: (value: A, options?: ConfigEncodeOptions) => Effect.Effect<string, ConfigEncodeError>;
|
|
649
692
|
/**
|
|
650
693
|
* Encode `value` and write it to an explicit `path`.
|
|
651
694
|
*
|
|
652
695
|
* @remarks
|
|
653
696
|
* Does **not** create the parent directory — that is
|
|
654
697
|
* {@link ConfigFileShape.save}'s job, and the distinction is load-bearing:
|
|
655
|
-
* `write` targets a path the caller already vouched for.
|
|
698
|
+
* `write` targets a path the caller already vouched for. `options` are the
|
|
699
|
+
* same as {@link ConfigFileShape.encode}'s, so the two stay in lockstep.
|
|
656
700
|
*/
|
|
657
|
-
readonly write: (value: A, path: string) => Effect.Effect<void, ConfigWriteError>;
|
|
701
|
+
readonly write: (value: A, path: string, options?: ConfigEncodeOptions) => Effect.Effect<void, ConfigWriteError>;
|
|
658
702
|
/**
|
|
659
703
|
* Resolve `defaultPath`, `mkdir -p` its parent, encode `value` into it, and
|
|
660
704
|
* return the path written.
|
|
@@ -1231,5 +1275,5 @@ export declare const TomlCodec: ConfigCodec;
|
|
|
1231
1275
|
*/
|
|
1232
1276
|
export declare const YamlCodec: ConfigCodec;
|
|
1233
1277
|
//#endregion
|
|
1234
|
-
export type { ConfigCodec, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMatch, ConfigMigrationOptions, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources, UpwardWalkOptions };
|
|
1278
|
+
export type { ConfigCodec, ConfigEncodeError, ConfigEncodeOptions, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMatch, ConfigMigrationOptions, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources, UpwardWalkOptions };
|
|
1235
1279
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/config-file",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable config file loading for Effect: JSON, JSONC, YAML and TOML codecs, resolution strategies, and merge behaviors.",
|
|
6
6
|
"keywords": [
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@effected/jsonc": "^0.11.0",
|
|
47
47
|
"@effected/toml": "^0.7.0",
|
|
48
|
-
"@effected/walker": "^0.
|
|
49
|
-
"@effected/yaml": "^0.15.
|
|
48
|
+
"@effected/walker": "^0.9.0",
|
|
49
|
+
"@effected/yaml": "^0.15.1",
|
|
50
50
|
"effect": "4.0.0-rc.115"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|