@rezwanul7/settings 0.1.0 → 1.0.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/README.md +20 -10
- package/dist/index.cjs +35 -1
- package/dist/index.d.cts +22 -9
- package/dist/index.d.ts +22 -9
- package/dist/index.js +35 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,15 +76,18 @@ the schema again breaks:
|
|
|
76
76
|
- Don't use schema-level defaults, such as Zod's `.default()`. The Settings definition's `defaults` are the one place
|
|
77
77
|
defaults live, and every field already has one there.
|
|
78
78
|
- Use only JSON values: strings, finite numbers, booleans, `null`, arrays and plain objects. Every Store keeps the
|
|
79
|
-
Overrides as JSON, so a
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
Overrides as JSON, so a write with anything else, such as a `Date`, a class instance, a `Map`, `NaN` or an array
|
|
80
|
+
hole, throws `SettingsValidationError` with a `"Not a JSON value"` issue at its path, even where the schema accepts
|
|
81
|
+
it. For a date, store the string and convert it where the setting is used.
|
|
82
82
|
|
|
83
83
|
## Creating the settings
|
|
84
84
|
|
|
85
|
-
Pass every Settings definition, by Scope name, and a Store to `createSettings`. Then `scope(name, id)`
|
|
86
|
-
Owner's `ScopedSettings
|
|
87
|
-
both rules are checked by the types.
|
|
85
|
+
Pass every Settings definition, by Scope name, and a Store to `createSettings`. Then `scope(name, id)` returns one
|
|
86
|
+
Owner's settings, a `ScopedSettings`: `scope("user", "42")` is user `42`, not every user. Owner ids are strings. A
|
|
87
|
+
single-owner Scope takes no id; any other Scope requires one, and both rules are checked by the types.
|
|
88
|
+
|
|
89
|
+
The Scope name is stored with every Owner's Overrides. Renaming a Scope, such as `user` to `member`, leaves the stored
|
|
90
|
+
rows under the old name, so its Owners read the defaults until the rows are migrated to the new name.
|
|
88
91
|
|
|
89
92
|
```ts
|
|
90
93
|
import { createSettings, MemoryStore } from "@rezwanul7/settings";
|
|
@@ -140,7 +143,7 @@ await me.reset(); // deletes everything stored for the Owner
|
|
|
140
143
|
| `get(path)` | One typed value of the Effective settings, at a dot path such as `"notifications.email"`. |
|
|
141
144
|
| `set(path, value)` | Stores `value` as the Override at `path`, keeping the other Overrides. |
|
|
142
145
|
| `update(partial)` | Deep-merges `partial` into the Overrides, to change several settings at once. |
|
|
143
|
-
| `getOverrides()` | The
|
|
146
|
+
| `getOverrides()` | The Owner's own Overrides, without the defaults, or `{}` when nothing is stored. |
|
|
144
147
|
| `reset(path)` | Removes the Override at `path` (and every Override under it), so the default applies again. |
|
|
145
148
|
| `reset()` | Deletes everything stored for the Owner, so it reads the defaults. |
|
|
146
149
|
|
|
@@ -151,15 +154,16 @@ await me.reset(); // deletes everything stored for the Owner
|
|
|
151
154
|
issue, and `get(path)` reads one as `undefined`. Stored ones are ignored as unknown keys.
|
|
152
155
|
- Merging works the same on reads and in `update`: plain objects merge key by key; arrays, scalars and `null` replace.
|
|
153
156
|
- In `update`, an `undefined` value leaves that Override as it was, and `null` is stored as a value, which passes only
|
|
154
|
-
if the schema allows it.
|
|
157
|
+
if the schema allows it. `set(path, undefined)` throws `SettingsValidationError`; use `reset(path)` to remove an
|
|
158
|
+
Override.
|
|
155
159
|
- A value equal to the current default is still stored as an Override, so a later change of the default doesn't change
|
|
156
160
|
what the Owner chose. `reset(path)` is how an Owner follows the default again; with no Override at `path`, it
|
|
157
161
|
writes nothing.
|
|
158
162
|
- A write whose Effective settings fail the schema throws `SettingsValidationError` and stores nothing. So does a
|
|
159
163
|
write with a key the schema doesn't know, which a schema that strips unknown keys would otherwise drop silently; the
|
|
160
164
|
issue for it has the message `"Unknown setting"`, as on read.
|
|
161
|
-
- `getOverrides()` suits a settings page that marks which values are the Owner's own
|
|
162
|
-
|
|
165
|
+
- `getOverrides()` suits a settings page that marks which values are the Owner's own. It leaves out the Overrides a
|
|
166
|
+
read would ignore, the same as `get()`, so the two always agree, and it is typed like the partial `update` takes.
|
|
163
167
|
|
|
164
168
|
### Authorize the Owner first
|
|
165
169
|
|
|
@@ -279,6 +283,8 @@ the Owner's page ([ADR-0001](./docs/adr/0001-overrides-only-ignored-when-invalid
|
|
|
279
283
|
to ignore. The rest of the Owner's Overrides still apply.
|
|
280
284
|
- `onInvalid(owner, issues)` is called with the issues naming what was ignored; an unknown key is reported as an issue
|
|
281
285
|
with the message `"Unknown setting"`. Without `onInvalid`, they are ignored silently, so pass it to log them.
|
|
286
|
+
- `getOverrides()` ignores them the same way, but returns the Overrides it kept instead of throwing when the
|
|
287
|
+
Effective settings still fail, so a page can show and fix them.
|
|
282
288
|
- A read never changes the stored document. The next successful write for that Owner drops the ignored Overrides.
|
|
283
289
|
- Writes read the Overrides first, so `onInvalid` can also be called during `set`, `update` or `reset(path)`, and more
|
|
284
290
|
than once for one write when it retries after a conflict.
|
|
@@ -380,6 +386,10 @@ try {
|
|
|
380
386
|
}
|
|
381
387
|
```
|
|
382
388
|
|
|
389
|
+
Each error sets `name` to its class name. When the app may load both the ESM and the CommonJS build of the package,
|
|
390
|
+
as mixed bundling can, `instanceof` fails across the two copies; check `error.name === "SettingsValidationError"`
|
|
391
|
+
there instead.
|
|
392
|
+
|
|
383
393
|
## API reference
|
|
384
394
|
|
|
385
395
|
Every export, parameter, and default is listed in the [API reference](./docs/api/README.md), generated from the source.
|
package/dist/index.cjs
CHANGED
|
@@ -140,6 +140,25 @@ function forbiddenKeysIn(value, path) {
|
|
|
140
140
|
([key2, child]) => FORBIDDEN_KEYS.has(key2) ? [{ message: "Forbidden key", path: [...path, key2] }] : forbiddenKeysIn(child, [...path, key2])
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
|
+
function nonJsonValues(path, value) {
|
|
144
|
+
return nonJsonValuesIn(value, path === void 0 ? [] : keysOf(path));
|
|
145
|
+
}
|
|
146
|
+
function nonJsonValuesIn(value, path) {
|
|
147
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
return Array.from(value).flatMap(
|
|
152
|
+
(item, index) => nonJsonValuesIn(item, [...path, index])
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (isPlainObject(value) && Object.getOwnPropertySymbols(value).length === 0) {
|
|
156
|
+
return Object.entries(value).flatMap(
|
|
157
|
+
([key2, child]) => child === void 0 ? [] : nonJsonValuesIn(child, [...path, key2])
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return [{ message: "Not a JSON value", path: [...path] }];
|
|
161
|
+
}
|
|
143
162
|
function getAt(value, path) {
|
|
144
163
|
let current = value;
|
|
145
164
|
for (const key2 of keysOf(path)) {
|
|
@@ -349,16 +368,27 @@ function createSettings(options) {
|
|
|
349
368
|
},
|
|
350
369
|
async set(path, value) {
|
|
351
370
|
rejectForbiddenKeys(path, value);
|
|
371
|
+
if (value === void 0) {
|
|
372
|
+
throw new SettingsValidationError([
|
|
373
|
+
{
|
|
374
|
+
message: "Not a JSON value: use reset(path) to remove an Override",
|
|
375
|
+
path: path.split(".")
|
|
376
|
+
}
|
|
377
|
+
]);
|
|
378
|
+
}
|
|
379
|
+
rejectNonJsonValues(path, value);
|
|
352
380
|
await change((overrides) => setAt(overrides, path, value));
|
|
353
381
|
},
|
|
354
382
|
async update(partial) {
|
|
355
383
|
rejectForbiddenKeys(void 0, partial);
|
|
384
|
+
rejectNonJsonValues(void 0, partial);
|
|
356
385
|
await change(
|
|
357
386
|
(overrides) => mergeOverrides(overrides, withoutUndefined(partial))
|
|
358
387
|
);
|
|
359
388
|
},
|
|
360
389
|
async getOverrides() {
|
|
361
|
-
|
|
390
|
+
const stored = await readOverrides();
|
|
391
|
+
return (await withoutStale(stored?.overrides ?? {})).overrides;
|
|
362
392
|
},
|
|
363
393
|
async reset(path) {
|
|
364
394
|
if (path === void 0) {
|
|
@@ -393,6 +423,10 @@ function rejectForbiddenKeys(path, value) {
|
|
|
393
423
|
const issues = forbiddenKeys(path, value);
|
|
394
424
|
if (issues.length > 0) throw new SettingsValidationError(issues);
|
|
395
425
|
}
|
|
426
|
+
function rejectNonJsonValues(path, value) {
|
|
427
|
+
const issues = nonJsonValues(path, value);
|
|
428
|
+
if (issues.length > 0) throw new SettingsValidationError(issues);
|
|
429
|
+
}
|
|
396
430
|
function unknownSettings(overrides, output) {
|
|
397
431
|
return unknownKeys(overrides, output).map((path) => ({
|
|
398
432
|
message: "Unknown setting",
|
package/dist/index.d.cts
CHANGED
|
@@ -180,6 +180,10 @@ interface SettingsOptions<Definitions extends Record<string, SettingsDefinition>
|
|
|
180
180
|
* The keys `__proto__`, `constructor` and `prototype` can't be setting names: a write with one in its path or value
|
|
181
181
|
* throws {@link SettingsValidationError} with a `"Forbidden key"` issue, and `get(path)` reads one as `undefined`.
|
|
182
182
|
*
|
|
183
|
+
* Overrides are kept as JSON, so a write with a value that isn't JSON, such as a `Date`, a class instance, `NaN` or an
|
|
184
|
+
* array hole, throws {@link SettingsValidationError} with a `"Not a JSON value"` issue at its path, even where the
|
|
185
|
+
* schema accepts it.
|
|
186
|
+
*
|
|
183
187
|
* @typeParam T - The Scope's settings type.
|
|
184
188
|
*/
|
|
185
189
|
interface ScopedSettings<T> {
|
|
@@ -208,9 +212,11 @@ interface ScopedSettings<T> {
|
|
|
208
212
|
* still stored, so a later change of the default doesn't change what the Owner chose.
|
|
209
213
|
*
|
|
210
214
|
* @param path - A dot path into the settings, such as `"notifications.email"`.
|
|
211
|
-
* @param value - The value to store at `path`.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
215
|
+
* @param value - The value to store at `path`. Use {@link ScopedSettings.reset} rather than `undefined` to remove an
|
|
216
|
+
* Override.
|
|
217
|
+
* @throws {@link SettingsValidationError} If the Effective settings with the change fail the schema, the change has
|
|
218
|
+
* a key the schema doesn't know, reported as an `"Unknown setting"` issue, or `value` is `undefined` or isn't JSON;
|
|
219
|
+
* nothing is stored.
|
|
214
220
|
* @throws {@link SettingsConflictError} If other writes to the Owner kept winning the version race for `maxRetries`
|
|
215
221
|
* retries; nothing is stored.
|
|
216
222
|
*/
|
|
@@ -221,19 +227,23 @@ interface ScopedSettings<T> {
|
|
|
221
227
|
* stored as a value and passes only if the schema allows it.
|
|
222
228
|
*
|
|
223
229
|
* @param partial - The settings to change, at any depth, such as `{ theme: "dark", notifications: { email: false } }`.
|
|
224
|
-
* @throws {@link SettingsValidationError} If the Effective settings with the change fail the schema,
|
|
225
|
-
* a key the schema doesn't know, reported as an `"Unknown setting"` issue; nothing is
|
|
230
|
+
* @throws {@link SettingsValidationError} If the Effective settings with the change fail the schema, `partial` has
|
|
231
|
+
* a key the schema doesn't know, reported as an `"Unknown setting"` issue, or a value in it isn't JSON; nothing is
|
|
232
|
+
* stored.
|
|
226
233
|
* @throws {@link SettingsConflictError} If other writes to the Owner kept winning the version race for `maxRetries`
|
|
227
234
|
* retries; nothing is stored.
|
|
228
235
|
*/
|
|
229
236
|
update(partial: DeepPartial<T>): Promise<void>;
|
|
230
237
|
/**
|
|
231
|
-
* Reads the Owner's Overrides
|
|
232
|
-
*
|
|
238
|
+
* Reads the Owner's Overrides without the defaults, for example to show which values are the Owner's own choices.
|
|
239
|
+
* Overrides a read would ignore are left out and reported to `onInvalid`, the same as {@link ScopedSettings.get},
|
|
240
|
+
* so the two always agree; the stored document isn't changed. Unlike `get`, it doesn't throw when the Effective
|
|
241
|
+
* settings still fail after leaving those out, so a page can show the Overrides that remain and fix them.
|
|
233
242
|
*
|
|
234
|
-
* @returns The
|
|
243
|
+
* @returns The Overrides a read uses, or `{}` when nothing is stored.
|
|
244
|
+
* @throws {@link SettingsDefinitionError} If the Scope's defaults fail its schema.
|
|
235
245
|
*/
|
|
236
|
-
getOverrides(): Promise<
|
|
246
|
+
getOverrides(): Promise<DeepPartial<T>>;
|
|
237
247
|
/**
|
|
238
248
|
* Removes the Override at `path`, so that setting follows the default again. Resetting an object removes every
|
|
239
249
|
* Override under it. With no Override at or under `path`, it writes nothing.
|
|
@@ -257,6 +267,9 @@ type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: st
|
|
|
257
267
|
* Creates the settings for an app's Scopes over one Store. Call `scope(name, id)` on the result to read and write
|
|
258
268
|
* one Owner's settings; a single-owner Scope takes no id.
|
|
259
269
|
*
|
|
270
|
+
* The Scope name is stored with every Owner's Overrides, so renaming a Scope needs a migration of its stored rows;
|
|
271
|
+
* until then its Owners read the defaults.
|
|
272
|
+
*
|
|
260
273
|
* Each Scope's defaults are validated against its schema on the first operation on that Scope, and the outcome is
|
|
261
274
|
* kept for the life of the returned object. If the validation itself throws, such as an async validator that can't
|
|
262
275
|
* reach its service, that operation rejects with the error and the next one checks again.
|
package/dist/index.d.ts
CHANGED
|
@@ -180,6 +180,10 @@ interface SettingsOptions<Definitions extends Record<string, SettingsDefinition>
|
|
|
180
180
|
* The keys `__proto__`, `constructor` and `prototype` can't be setting names: a write with one in its path or value
|
|
181
181
|
* throws {@link SettingsValidationError} with a `"Forbidden key"` issue, and `get(path)` reads one as `undefined`.
|
|
182
182
|
*
|
|
183
|
+
* Overrides are kept as JSON, so a write with a value that isn't JSON, such as a `Date`, a class instance, `NaN` or an
|
|
184
|
+
* array hole, throws {@link SettingsValidationError} with a `"Not a JSON value"` issue at its path, even where the
|
|
185
|
+
* schema accepts it.
|
|
186
|
+
*
|
|
183
187
|
* @typeParam T - The Scope's settings type.
|
|
184
188
|
*/
|
|
185
189
|
interface ScopedSettings<T> {
|
|
@@ -208,9 +212,11 @@ interface ScopedSettings<T> {
|
|
|
208
212
|
* still stored, so a later change of the default doesn't change what the Owner chose.
|
|
209
213
|
*
|
|
210
214
|
* @param path - A dot path into the settings, such as `"notifications.email"`.
|
|
211
|
-
* @param value - The value to store at `path`.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
215
|
+
* @param value - The value to store at `path`. Use {@link ScopedSettings.reset} rather than `undefined` to remove an
|
|
216
|
+
* Override.
|
|
217
|
+
* @throws {@link SettingsValidationError} If the Effective settings with the change fail the schema, the change has
|
|
218
|
+
* a key the schema doesn't know, reported as an `"Unknown setting"` issue, or `value` is `undefined` or isn't JSON;
|
|
219
|
+
* nothing is stored.
|
|
214
220
|
* @throws {@link SettingsConflictError} If other writes to the Owner kept winning the version race for `maxRetries`
|
|
215
221
|
* retries; nothing is stored.
|
|
216
222
|
*/
|
|
@@ -221,19 +227,23 @@ interface ScopedSettings<T> {
|
|
|
221
227
|
* stored as a value and passes only if the schema allows it.
|
|
222
228
|
*
|
|
223
229
|
* @param partial - The settings to change, at any depth, such as `{ theme: "dark", notifications: { email: false } }`.
|
|
224
|
-
* @throws {@link SettingsValidationError} If the Effective settings with the change fail the schema,
|
|
225
|
-
* a key the schema doesn't know, reported as an `"Unknown setting"` issue; nothing is
|
|
230
|
+
* @throws {@link SettingsValidationError} If the Effective settings with the change fail the schema, `partial` has
|
|
231
|
+
* a key the schema doesn't know, reported as an `"Unknown setting"` issue, or a value in it isn't JSON; nothing is
|
|
232
|
+
* stored.
|
|
226
233
|
* @throws {@link SettingsConflictError} If other writes to the Owner kept winning the version race for `maxRetries`
|
|
227
234
|
* retries; nothing is stored.
|
|
228
235
|
*/
|
|
229
236
|
update(partial: DeepPartial<T>): Promise<void>;
|
|
230
237
|
/**
|
|
231
|
-
* Reads the Owner's Overrides
|
|
232
|
-
*
|
|
238
|
+
* Reads the Owner's Overrides without the defaults, for example to show which values are the Owner's own choices.
|
|
239
|
+
* Overrides a read would ignore are left out and reported to `onInvalid`, the same as {@link ScopedSettings.get},
|
|
240
|
+
* so the two always agree; the stored document isn't changed. Unlike `get`, it doesn't throw when the Effective
|
|
241
|
+
* settings still fail after leaving those out, so a page can show the Overrides that remain and fix them.
|
|
233
242
|
*
|
|
234
|
-
* @returns The
|
|
243
|
+
* @returns The Overrides a read uses, or `{}` when nothing is stored.
|
|
244
|
+
* @throws {@link SettingsDefinitionError} If the Scope's defaults fail its schema.
|
|
235
245
|
*/
|
|
236
|
-
getOverrides(): Promise<
|
|
246
|
+
getOverrides(): Promise<DeepPartial<T>>;
|
|
237
247
|
/**
|
|
238
248
|
* Removes the Override at `path`, so that setting follows the default again. Resetting an object removes every
|
|
239
249
|
* Override under it. With no Override at or under `path`, it writes nothing.
|
|
@@ -257,6 +267,9 @@ type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: st
|
|
|
257
267
|
* Creates the settings for an app's Scopes over one Store. Call `scope(name, id)` on the result to read and write
|
|
258
268
|
* one Owner's settings; a single-owner Scope takes no id.
|
|
259
269
|
*
|
|
270
|
+
* The Scope name is stored with every Owner's Overrides, so renaming a Scope needs a migration of its stored rows;
|
|
271
|
+
* until then its Owners read the defaults.
|
|
272
|
+
*
|
|
260
273
|
* Each Scope's defaults are validated against its schema on the first operation on that Scope, and the outcome is
|
|
261
274
|
* kept for the life of the returned object. If the validation itself throws, such as an async validator that can't
|
|
262
275
|
* reach its service, that operation rejects with the error and the next one checks again.
|
package/dist/index.js
CHANGED
|
@@ -138,6 +138,25 @@ function forbiddenKeysIn(value, path) {
|
|
|
138
138
|
([key2, child]) => FORBIDDEN_KEYS.has(key2) ? [{ message: "Forbidden key", path: [...path, key2] }] : forbiddenKeysIn(child, [...path, key2])
|
|
139
139
|
);
|
|
140
140
|
}
|
|
141
|
+
function nonJsonValues(path, value) {
|
|
142
|
+
return nonJsonValuesIn(value, path === void 0 ? [] : keysOf(path));
|
|
143
|
+
}
|
|
144
|
+
function nonJsonValuesIn(value, path) {
|
|
145
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
return Array.from(value).flatMap(
|
|
150
|
+
(item, index) => nonJsonValuesIn(item, [...path, index])
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (isPlainObject(value) && Object.getOwnPropertySymbols(value).length === 0) {
|
|
154
|
+
return Object.entries(value).flatMap(
|
|
155
|
+
([key2, child]) => child === void 0 ? [] : nonJsonValuesIn(child, [...path, key2])
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return [{ message: "Not a JSON value", path: [...path] }];
|
|
159
|
+
}
|
|
141
160
|
function getAt(value, path) {
|
|
142
161
|
let current = value;
|
|
143
162
|
for (const key2 of keysOf(path)) {
|
|
@@ -347,16 +366,27 @@ function createSettings(options) {
|
|
|
347
366
|
},
|
|
348
367
|
async set(path, value) {
|
|
349
368
|
rejectForbiddenKeys(path, value);
|
|
369
|
+
if (value === void 0) {
|
|
370
|
+
throw new SettingsValidationError([
|
|
371
|
+
{
|
|
372
|
+
message: "Not a JSON value: use reset(path) to remove an Override",
|
|
373
|
+
path: path.split(".")
|
|
374
|
+
}
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
rejectNonJsonValues(path, value);
|
|
350
378
|
await change((overrides) => setAt(overrides, path, value));
|
|
351
379
|
},
|
|
352
380
|
async update(partial) {
|
|
353
381
|
rejectForbiddenKeys(void 0, partial);
|
|
382
|
+
rejectNonJsonValues(void 0, partial);
|
|
354
383
|
await change(
|
|
355
384
|
(overrides) => mergeOverrides(overrides, withoutUndefined(partial))
|
|
356
385
|
);
|
|
357
386
|
},
|
|
358
387
|
async getOverrides() {
|
|
359
|
-
|
|
388
|
+
const stored = await readOverrides();
|
|
389
|
+
return (await withoutStale(stored?.overrides ?? {})).overrides;
|
|
360
390
|
},
|
|
361
391
|
async reset(path) {
|
|
362
392
|
if (path === void 0) {
|
|
@@ -391,6 +421,10 @@ function rejectForbiddenKeys(path, value) {
|
|
|
391
421
|
const issues = forbiddenKeys(path, value);
|
|
392
422
|
if (issues.length > 0) throw new SettingsValidationError(issues);
|
|
393
423
|
}
|
|
424
|
+
function rejectNonJsonValues(path, value) {
|
|
425
|
+
const issues = nonJsonValues(path, value);
|
|
426
|
+
if (issues.length > 0) throw new SettingsValidationError(issues);
|
|
427
|
+
}
|
|
394
428
|
function unknownSettings(overrides, output) {
|
|
395
429
|
return unknownKeys(overrides, output).map((path) => ({
|
|
396
430
|
message: "Unknown setting",
|