@effected/app 0.9.1 → 0.10.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/App.js +6 -0
- package/AppConfig.js +18 -4
- package/README.md +53 -0
- package/index.d.ts +80 -4
- package/package.json +4 -4
package/App.js
CHANGED
|
@@ -53,6 +53,12 @@ var App = class {
|
|
|
53
53
|
* reuse that binding. Calling it inline at two provide sites opens two
|
|
54
54
|
* databases — two connections onto one file, two migration ledgers, and two
|
|
55
55
|
* independent `CacheEvent` PubSubs whose subscribers each see half the events.
|
|
56
|
+
*
|
|
57
|
+
* Testing cache expiry against this layer has one ordering rule: provide
|
|
58
|
+
* `TestClock.layer()` **outside** the `Effect.provide` that supplies this
|
|
59
|
+
* layer, never beneath it. Underneath, the test body has no `TestClock` in
|
|
60
|
+
* its own context and `TestClock.adjust` dies as a defect — so the entries
|
|
61
|
+
* under test carry real timestamps and nothing ever expires.
|
|
56
62
|
*/
|
|
57
63
|
static layer = layer;
|
|
58
64
|
/**
|
package/AppConfig.js
CHANGED
|
@@ -8,10 +8,14 @@ const layer = (tag, options) => Layer.unwrap(Effect.gen(function* () {
|
|
|
8
8
|
const invalid = badFilename("AppConfig.layer", options.filename);
|
|
9
9
|
if (invalid !== void 0) return yield* Effect.die(invalid);
|
|
10
10
|
const appDirs = yield* AppDirs;
|
|
11
|
-
const resolvers =
|
|
12
|
-
|
|
13
|
-
filename: options.filename
|
|
14
|
-
|
|
11
|
+
const resolvers = [
|
|
12
|
+
...options.resolvers ?? [],
|
|
13
|
+
XdgConfig.resolver({ filename: options.filename }),
|
|
14
|
+
...options.native === false ? [] : [XdgConfig.nativeResolver({
|
|
15
|
+
namespace: appDirs.namespace,
|
|
16
|
+
filename: options.filename
|
|
17
|
+
})]
|
|
18
|
+
];
|
|
15
19
|
return ConfigFile.layer(tag, {
|
|
16
20
|
schema: options.schema,
|
|
17
21
|
codec: options.codec,
|
|
@@ -19,6 +23,7 @@ const layer = (tag, options) => Layer.unwrap(Effect.gen(function* () {
|
|
|
19
23
|
resolvers,
|
|
20
24
|
defaultPath: XdgConfig.savePath(options.filename),
|
|
21
25
|
...options.validate !== void 0 && { validate: options.validate },
|
|
26
|
+
...options.parseOptions !== void 0 && { parseOptions: options.parseOptions },
|
|
22
27
|
...options.events !== void 0 && { events: options.events }
|
|
23
28
|
});
|
|
24
29
|
}));
|
|
@@ -47,6 +52,15 @@ var AppConfig = class {
|
|
|
47
52
|
* config-file's infallible `defaultPath` slot without an `orDie` because xdg
|
|
48
53
|
* resolves at layer-construction time.
|
|
49
54
|
*
|
|
55
|
+
* `options.resolvers` prepends to that chain, which covers the case this
|
|
56
|
+
* preset otherwise could not: a CLI whose `--config` flag must outrank the
|
|
57
|
+
* app's XDG search path. What it deliberately does not cover is a chain that
|
|
58
|
+
* needs the XDG resolvers somewhere other than last, or no XDG resolvers at
|
|
59
|
+
* all — an app wanting that composes `ConfigFile.layer` from
|
|
60
|
+
* `@effected/config-file` directly and orders the whole chain itself, which
|
|
61
|
+
* costs it only the `defaultPath` and ambient-namespace wiring this preset
|
|
62
|
+
* does for free.
|
|
63
|
+
*
|
|
50
64
|
* **The namespace is never a parameter.** It is read from the ambient
|
|
51
65
|
* `AppDirs` service at layer build time, so it is typed exactly once, in
|
|
52
66
|
* `App.layer` — the two-strings drift where an app passes `"myapp"` to
|
package/README.md
CHANGED
|
@@ -121,6 +121,59 @@ What it does take is a **codec, required**, never inferred from the filename's e
|
|
|
121
121
|
|
|
122
122
|
`AppConfig` lives in its own module and reaches `@effected/xdg` and `@effected/config-file` **only** — never `@effected/store`. An application that wants XDG-placed config files and no database imports `AppConfig` alone, and no SQLite driver enters its graph. `App`, `AppStore` and `AppCache` are the exports that reach a database, and keeping the two graphs apart is why there is no `App = { … }` namespace object here.
|
|
123
123
|
|
|
124
|
+
## A `--config` flag, without leaving the preset
|
|
125
|
+
|
|
126
|
+
Most CLIs have one, and it has to outrank the app's own search path. Pass `resolvers` and it does:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { AppConfig } from "@effected/app";
|
|
130
|
+
import { ConfigResolver, TomlCodec } from "@effected/config-file";
|
|
131
|
+
|
|
132
|
+
const ConfigLive = AppConfig.layer(SettingsFile, {
|
|
133
|
+
filename: "settings.toml",
|
|
134
|
+
schema: Settings,
|
|
135
|
+
codec: TomlCodec,
|
|
136
|
+
// Composed AHEAD of the XDG chain, in the order given.
|
|
137
|
+
resolvers: flag === undefined ? [] : [ConfigResolver.explicitPath(flag)],
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The XDG search path and the native probe stay behind whatever you prepend, so absent the flag nothing changes. `ConfigResolver.staticDir` covers a `--config` naming a directory, and `ConfigResolver.upwardWalk` a project-local file found by walking up from the cwd.
|
|
142
|
+
|
|
143
|
+
Two properties to be deliberate about. **A resolver that finds nothing falls through**: every `ConfigResolver`'s error channel is `never` by contract, so a `--config` pointing at a file that does not exist quietly loads the XDG config instead. If that must be an error, check the path before you build the layer — discovery cannot make that distinction for you. And **the save path is unaffected**: `save` still writes to the app's own config directory, so writing back to the file a flag named is `write(value, path)`, which takes the path explicitly.
|
|
144
|
+
|
|
145
|
+
### Wiring the flag to the layer
|
|
146
|
+
|
|
147
|
+
A layer is built before a CLI parses anything, so the parsed `--config` has to reach `AppConfig.layer` somehow. `effect/unstable/cli` has two ways, and neither needs an `Effect.provide` inside the handler.
|
|
148
|
+
|
|
149
|
+
For a single command, `Command.provide` takes **a function of the parsed input**, not just a finished layer:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
Command.make("validate", { config: Flag.string("config").pipe(Flag.optional) }, () =>
|
|
153
|
+
Effect.gen(function* () {
|
|
154
|
+
const settings = yield* (yield* SettingsFile).load; // just requires the service
|
|
155
|
+
}),
|
|
156
|
+
).pipe(Command.provide(({ config }) => makeConfigLive(Option.getOrUndefined(config))));
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For several subcommands sharing one `--config`, make the flag a **global setting** — `GlobalFlag.setting` returns a `Context.Service`, so the parsed value can sit in the layer's `R` and the layer attaches once at the root:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const ConfigFlag = GlobalFlag.setting("config")({ flag: Flag.string("config").pipe(Flag.optional) });
|
|
163
|
+
|
|
164
|
+
const ConfigLive = Layer.unwrap(Effect.map(ConfigFlag, (f) => makeConfigLive(Option.getOrUndefined(f))));
|
|
165
|
+
|
|
166
|
+
const root = Command.make("myapp", {}, () => Effect.void).pipe(
|
|
167
|
+
Command.withSubcommands([validate, sync]),
|
|
168
|
+
Command.provide(ConfigLive), // one call site, every subcommand
|
|
169
|
+
Command.withGlobalFlags([ConfigFlag]),
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The flag is then accepted on either side of the subcommand name.
|
|
174
|
+
|
|
175
|
+
A chain that needs the XDG resolvers somewhere other than last — or not at all — has outgrown the preset. Compose `ConfigFile.layer` from [`@effected/config-file`](../config-file) directly and order the whole chain yourself; it costs you only the `defaultPath` and ambient-namespace wiring this preset does for free.
|
|
176
|
+
|
|
124
177
|
Its `native` option defaults to **`true`** — the opposite of `AppDirsOptions.native`, and the asymmetry is deliberate. *Creating* a native directory commits an application to a location, so it is opt-in; *probing* one for a config file the user already put there costs a `stat` that finds nothing, so it is opt-out. Reading `~/Library/Application Support` is a courtesy; writing there uninvited is not.
|
|
125
178
|
|
|
126
179
|
## Ensure before open
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Cache, CacheError, CacheOptions, Store, StoreError, StoreMigrationError, StoreOptions } from "@effected/store";
|
|
2
2
|
import { AppDirs, AppDirsError, AppDirsOptions, Xdg, XdgEnvError, XdgPaths } from "@effected/xdg";
|
|
3
|
-
import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
|
|
4
|
-
import { ConfigCodec, ConfigEvents, ConfigEventsShape, ConfigFileShape, ConfigValidationError, MergeStrategy } from "@effected/config-file";
|
|
3
|
+
import { Context, Effect, FileSystem, Layer, Path, Schema, SchemaAST } from "effect";
|
|
4
|
+
import { ConfigCodec, ConfigEvents, ConfigEventsShape, ConfigFileShape, ConfigResolver, ConfigValidationError, MergeStrategy } from "@effected/config-file";
|
|
5
5
|
//#region src/AppCache.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Options for {@link AppCache.layer}.
|
|
@@ -162,6 +162,12 @@ declare class App {
|
|
|
162
162
|
* reuse that binding. Calling it inline at two provide sites opens two
|
|
163
163
|
* databases — two connections onto one file, two migration ledgers, and two
|
|
164
164
|
* independent `CacheEvent` PubSubs whose subscribers each see half the events.
|
|
165
|
+
*
|
|
166
|
+
* Testing cache expiry against this layer has one ordering rule: provide
|
|
167
|
+
* `TestClock.layer()` **outside** the `Effect.provide` that supplies this
|
|
168
|
+
* layer, never beneath it. Underneath, the test body has no `TestClock` in
|
|
169
|
+
* its own context and `TestClock.adjust` dies as a defect — so the entries
|
|
170
|
+
* under test carry real timestamps and nothing ever expires.
|
|
165
171
|
*/
|
|
166
172
|
static readonly layer: (options: AppOptions) => Layer.Layer<Xdg | AppDirs | Store | Cache, AppError, FileSystem.FileSystem | Path.Path>;
|
|
167
173
|
/**
|
|
@@ -187,9 +193,14 @@ declare class App {
|
|
|
187
193
|
/**
|
|
188
194
|
* Options for {@link AppConfig.layer}.
|
|
189
195
|
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* `RR` is the requirements of any caller-supplied `resolvers`; it defaults to
|
|
198
|
+
* `never`, so a chain of built-in resolvers — whose requirements are already in
|
|
199
|
+
* this layer's `R` — never has to be named.
|
|
200
|
+
*
|
|
190
201
|
* @public
|
|
191
202
|
*/
|
|
192
|
-
interface AppConfigOptions<A, I> {
|
|
203
|
+
interface AppConfigOptions<A, I, RR = never> {
|
|
193
204
|
/**
|
|
194
205
|
* The config file's name within the app's config directory.
|
|
195
206
|
*
|
|
@@ -216,8 +227,64 @@ interface AppConfigOptions<A, I> {
|
|
|
216
227
|
readonly strategy?: MergeStrategy<A>;
|
|
217
228
|
/** An optional caller-supplied check run after schema decoding. */
|
|
218
229
|
readonly validate?: (value: A) => Effect.Effect<A, ConfigValidationError>;
|
|
230
|
+
/**
|
|
231
|
+
* Parse options threaded into every schema decode, chiefly
|
|
232
|
+
* `onExcessProperty`.
|
|
233
|
+
*
|
|
234
|
+
* @remarks
|
|
235
|
+
* Defaults to core's `"ignore"`, so a config file's unknown keys are dropped
|
|
236
|
+
* silently — which means a typo'd section cannot be reported, and a field
|
|
237
|
+
* this schema deliberately removed cannot be flagged in a user's older file.
|
|
238
|
+
* `{ onExcessProperty: "error" }` turns both into a `ConfigValidationError`
|
|
239
|
+
* naming the offending path.
|
|
240
|
+
*
|
|
241
|
+
* `validate` cannot do this: it runs on the decoded value, after the excess
|
|
242
|
+
* keys are gone. Keys covered by a `Schema.StructWithRest` rest are not
|
|
243
|
+
* excess, so a deliberate pass-through section still works under `"error"`.
|
|
244
|
+
*
|
|
245
|
+
* Pair it with `errors: "all"`. Core defaults to `"first"`, which for a
|
|
246
|
+
* *loader* means a file with three typos surfaces one per run — fix,
|
|
247
|
+
* re-run, discover the next. The extra work only happens on a document
|
|
248
|
+
* that is already failing.
|
|
249
|
+
*/
|
|
250
|
+
readonly parseOptions?: SchemaAST.ParseOptions;
|
|
219
251
|
/** The opt-in event hook. Pass the `ConfigEvents` class itself. */
|
|
220
252
|
readonly events?: Context.Key<ConfigEvents, ConfigEventsShape>;
|
|
253
|
+
/**
|
|
254
|
+
* Resolvers composed **ahead** of the XDG chain, in priority order.
|
|
255
|
+
*
|
|
256
|
+
* @remarks
|
|
257
|
+
* The case this exists for is a CLI's `--config` flag: pass
|
|
258
|
+
* `ConfigResolver.explicitPath` for a file, or `ConfigResolver.staticDir` for
|
|
259
|
+
* a directory, and the flag wins — with the app's XDG search path, and the
|
|
260
|
+
* native probe, still behind it as the fallback. `ConfigResolver.upwardWalk`
|
|
261
|
+
* for a project-local file goes here too.
|
|
262
|
+
*
|
|
263
|
+
* Absent, the chain is exactly what it always was, so the default is
|
|
264
|
+
* unchanged: `XdgConfig.resolver`, then `XdgConfig.nativeResolver`.
|
|
265
|
+
*
|
|
266
|
+
* A layer is built before a CLI parses anything, so getting the parsed flag
|
|
267
|
+
* here is the one wiring question this option raises. `effect/unstable/cli`
|
|
268
|
+
* answers it twice, and neither answer needs an `Effect.provide` inside the
|
|
269
|
+
* handler: `Command.provide` accepts a **function of the parsed input**, and
|
|
270
|
+
* for several subcommands sharing one flag, `GlobalFlag.setting` makes the
|
|
271
|
+
* parsed value a `Context.Service` so the layer can take it in `R` and attach
|
|
272
|
+
* once at the root. The README shows both.
|
|
273
|
+
*
|
|
274
|
+
* Two properties to be deliberate about:
|
|
275
|
+
*
|
|
276
|
+
* - **A resolver that finds nothing falls through.** `explicitPath` resolves
|
|
277
|
+
* `Option.none()` for a path that does not exist — every `ConfigResolver`'s
|
|
278
|
+
* error channel is `never` by contract — so a `--config` pointing at a
|
|
279
|
+
* missing file silently loads the XDG config instead. If the flag must be
|
|
280
|
+
* an error when it names nothing, check the path before building the layer;
|
|
281
|
+
* discovery cannot make that distinction for you.
|
|
282
|
+
* - **The save path is unaffected.** `save` still writes to
|
|
283
|
+
* `XdgConfig.savePath(filename)`, not to whatever a prepended resolver
|
|
284
|
+
* discovered. Loading from `--config` and writing back to that same file is
|
|
285
|
+
* `write(value, path)`, which takes the path explicitly.
|
|
286
|
+
*/
|
|
287
|
+
readonly resolvers?: ReadonlyArray<ConfigResolver<RR>>;
|
|
221
288
|
/**
|
|
222
289
|
* Probe the OS-native config directory as a fallback. Defaults to `true`.
|
|
223
290
|
*
|
|
@@ -253,6 +320,15 @@ declare class AppConfig {
|
|
|
253
320
|
* config-file's infallible `defaultPath` slot without an `orDie` because xdg
|
|
254
321
|
* resolves at layer-construction time.
|
|
255
322
|
*
|
|
323
|
+
* `options.resolvers` prepends to that chain, which covers the case this
|
|
324
|
+
* preset otherwise could not: a CLI whose `--config` flag must outrank the
|
|
325
|
+
* app's XDG search path. What it deliberately does not cover is a chain that
|
|
326
|
+
* needs the XDG resolvers somewhere other than last, or no XDG resolvers at
|
|
327
|
+
* all — an app wanting that composes `ConfigFile.layer` from
|
|
328
|
+
* `@effected/config-file` directly and orders the whole chain itself, which
|
|
329
|
+
* costs it only the `defaultPath` and ambient-namespace wiring this preset
|
|
330
|
+
* does for free.
|
|
331
|
+
*
|
|
256
332
|
* **The namespace is never a parameter.** It is read from the ambient
|
|
257
333
|
* `AppDirs` service at layer build time, so it is typed exactly once, in
|
|
258
334
|
* `App.layer` — the two-strings drift where an app passes `"myapp"` to
|
|
@@ -261,7 +337,7 @@ declare class AppConfig {
|
|
|
261
337
|
* This is a layer-returning function: bind the result to a `const` and reuse
|
|
262
338
|
* that binding, or two provide sites mint two independent service instances.
|
|
263
339
|
*/
|
|
264
|
-
static readonly layer: <Self, A, I>(tag: Context.Key<Self, ConfigFileShape<A>>, options: AppConfigOptions<A, I>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path | AppDirs | Xdg>;
|
|
340
|
+
static readonly layer: <Self, A, I, RR = never>(tag: Context.Key<Self, ConfigFileShape<A>>, options: AppConfigOptions<A, I, RR>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path | AppDirs | Xdg | RR>;
|
|
265
341
|
}
|
|
266
342
|
//#endregion
|
|
267
343
|
export { App, AppCache, type AppCacheOptions, AppConfig, type AppConfigOptions, type AppError, type AppOptions, AppStore, type AppStoreOptions, type AppTestOptions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The application control plane for Effect: one layer wiring XDG-namespaced directories, a migrated SQLite store, a TTL cache and a config file to the same place.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"./package.json": "./package.json"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@effected/config-file": "^0.
|
|
43
|
-
"@effected/store": "^0.
|
|
44
|
-
"@effected/xdg": "^0.2.
|
|
42
|
+
"@effected/config-file": "^0.4.0",
|
|
43
|
+
"@effected/store": "^0.3.0",
|
|
44
|
+
"@effected/xdg": "^0.2.1",
|
|
45
45
|
"effect": "4.0.0-beta.107"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|