@crustjs/store 0.0.2 → 0.0.3
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 +301 -138
- package/dist/index.d.ts +337 -212
- package/dist/index.js +3 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @crustjs/store
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DX-first, typed persistence for CLI apps with clear config/data/state/cache separation.
|
|
4
4
|
|
|
5
|
-
`@crustjs/store` gives your CLI
|
|
5
|
+
`@crustjs/store` gives your CLI production-ready local persistence with near-zero setup. Provide a defaults object, pick a storage intent, and get a typed store with read/write/update/patch/reset — no manual type annotations needed.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -26,156 +26,213 @@ import { createStore, configDir } from "@crustjs/store";
|
|
|
26
26
|
|
|
27
27
|
const store = createStore({
|
|
28
28
|
dirPath: configDir("my-cli"),
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
verbose:
|
|
32
|
-
token: { type: "string" }, // optional — no default
|
|
29
|
+
defaults: {
|
|
30
|
+
ui: { theme: "light" as string, fontSize: 14 },
|
|
31
|
+
verbose: false,
|
|
33
32
|
},
|
|
34
33
|
});
|
|
35
34
|
|
|
36
|
-
// Read
|
|
37
|
-
const
|
|
38
|
-
// → { theme: "light",
|
|
35
|
+
// Read state (returns defaults when no persisted file exists)
|
|
36
|
+
const state = await store.read();
|
|
37
|
+
// → { ui: { theme: "light", fontSize: 14 }, verbose: false }
|
|
39
38
|
|
|
40
|
-
// Write a full
|
|
41
|
-
await store.write({ theme: "dark",
|
|
39
|
+
// Write a full state object
|
|
40
|
+
await store.write({ ui: { theme: "dark", fontSize: 16 }, verbose: true });
|
|
42
41
|
|
|
43
|
-
// Update a
|
|
42
|
+
// Update with a function
|
|
44
43
|
await store.update((current) => ({ ...current, verbose: false }));
|
|
45
44
|
|
|
45
|
+
// Patch only specific nested keys
|
|
46
|
+
await store.patch({ ui: { theme: "solarized" } });
|
|
47
|
+
|
|
46
48
|
// Reset to defaults (removes persisted file)
|
|
47
49
|
await store.reset();
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
No explicit generics needed — types are inferred from your
|
|
52
|
+
No explicit generics needed — types are inferred from your `defaults` object.
|
|
53
|
+
|
|
54
|
+
## Storage Intent: Choosing a Directory
|
|
55
|
+
|
|
56
|
+
`@crustjs/store` provides four path helpers that resolve platform-standard directories for different storage intents. Use the one that matches what you're persisting:
|
|
57
|
+
|
|
58
|
+
| Helper | Intent | Example Use Case |
|
|
59
|
+
| ----------- | -------------------------- | --------------------------------------- |
|
|
60
|
+
| `configDir` | User preferences & config | Theme, editor settings, API keys |
|
|
61
|
+
| `dataDir` | Important app data | Local databases, downloaded resources |
|
|
62
|
+
| `stateDir` | Runtime state | Window positions, scroll offsets, undo |
|
|
63
|
+
| `cacheDir` | Regenerable cached data | HTTP caches, compiled assets |
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createStore, configDir, dataDir, stateDir, cacheDir } from "@crustjs/store";
|
|
67
|
+
|
|
68
|
+
// User preferences → ~/.config/my-cli/config.json
|
|
69
|
+
const config = createStore({
|
|
70
|
+
dirPath: configDir("my-cli"),
|
|
71
|
+
defaults: { theme: "light" as string, verbose: false },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// App data → ~/.local/share/my-cli/config.json
|
|
75
|
+
const data = createStore({
|
|
76
|
+
dirPath: dataDir("my-cli"),
|
|
77
|
+
defaults: { bookmarks: [] as string[] },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Runtime state → ~/.local/state/my-cli/config.json
|
|
81
|
+
const state = createStore({
|
|
82
|
+
dirPath: stateDir("my-cli"),
|
|
83
|
+
defaults: { lastOpened: "" as string, scrollY: 0 },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Cache → ~/.cache/my-cli/config.json
|
|
87
|
+
const cache = createStore({
|
|
88
|
+
dirPath: cacheDir("my-cli"),
|
|
89
|
+
defaults: { etag: "" as string, payload: "" as string },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Platform Paths
|
|
94
|
+
|
|
95
|
+
All four helpers follow XDG conventions on Linux and macOS, and Windows-native conventions on Windows:
|
|
96
|
+
|
|
97
|
+
| Helper | Linux / macOS | Windows | Env Override |
|
|
98
|
+
| ----------- | ----------------------------------- | ------------------------------------------- | -------------------- |
|
|
99
|
+
| `configDir` | `~/.config/<app>` | `%APPDATA%\<app>` | `$XDG_CONFIG_HOME` |
|
|
100
|
+
| `dataDir` | `~/.local/share/<app>` | `%LOCALAPPDATA%\<app>\Data` | `$XDG_DATA_HOME` |
|
|
101
|
+
| `stateDir` | `~/.local/state/<app>` | `%LOCALAPPDATA%\<app>\State` | `$XDG_STATE_HOME` |
|
|
102
|
+
| `cacheDir` | `~/.cache/<app>` | `%LOCALAPPDATA%\<app>\Cache` | `$XDG_CACHE_HOME` |
|
|
103
|
+
|
|
104
|
+
macOS uses XDG conventions (same as Linux) for a unified Unix mental model.
|
|
51
105
|
|
|
52
106
|
## API
|
|
53
107
|
|
|
54
108
|
### `createStore(options)`
|
|
55
109
|
|
|
56
|
-
Creates a typed async
|
|
110
|
+
Creates a typed async store backed by a local JSON file. The file path is resolved once at creation time.
|
|
57
111
|
|
|
58
112
|
```ts
|
|
59
113
|
const store = createStore(options);
|
|
60
114
|
```
|
|
61
115
|
|
|
62
|
-
**Returns:** A `Store<
|
|
116
|
+
**Returns:** A `Store<T>` with `read()`, `write()`, `update()`, `patch()`, and `reset()` methods.
|
|
63
117
|
|
|
64
118
|
**Throws:** `CrustStoreError` with `PATH` code if `dirPath` or `name` is invalid.
|
|
65
119
|
|
|
66
120
|
#### Options
|
|
67
121
|
|
|
68
|
-
| Option
|
|
69
|
-
|
|
|
70
|
-
| `dirPath`
|
|
71
|
-
| `name`
|
|
72
|
-
| `
|
|
122
|
+
| Option | Type | Required | Description |
|
|
123
|
+
| -------------- | -------------------------- | -------- | ------------------------------------------------------------------ |
|
|
124
|
+
| `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
|
|
125
|
+
| `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
|
|
126
|
+
| `defaults` | `T` | Yes | Default values defining the store's data shape and fallbacks. |
|
|
127
|
+
| `validator` | `StoreValidator<T>` | No | Validation function applied on read, write, update, and patch. |
|
|
128
|
+
| `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
|
|
73
129
|
|
|
74
|
-
### `
|
|
130
|
+
### `store.read()`
|
|
75
131
|
|
|
76
|
-
|
|
132
|
+
Reads the persisted state file. Missing keys are filled from `defaults` via deep merge. When no file exists, returns `defaults`.
|
|
77
133
|
|
|
78
134
|
```ts
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const dir = configDir("my-cli");
|
|
82
|
-
// → "/home/user/.config/my-cli" (Linux)
|
|
83
|
-
// → "/Users/user/Library/Application Support/my-cli" (macOS)
|
|
84
|
-
// → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
|
|
135
|
+
const state = await store.read();
|
|
85
136
|
```
|
|
86
137
|
|
|
87
|
-
|
|
138
|
+
Always returns a value — never `undefined`. Does not write merged defaults back to disk.
|
|
139
|
+
|
|
140
|
+
### `store.write(state)`
|
|
141
|
+
|
|
142
|
+
Atomically persists a full state object. The entire previous state is replaced.
|
|
88
143
|
|
|
89
144
|
```ts
|
|
90
|
-
|
|
91
|
-
dirPath: configDir("my-cli"),
|
|
92
|
-
fields: { /* ... */ },
|
|
93
|
-
});
|
|
145
|
+
await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
|
|
94
146
|
```
|
|
95
147
|
|
|
96
|
-
|
|
148
|
+
Calls `validator` (if provided) before writing. Parent directories are created if missing.
|
|
97
149
|
|
|
98
|
-
|
|
150
|
+
### `store.update(updater)`
|
|
99
151
|
|
|
100
|
-
|
|
101
|
-
| ------------- | --------------------------------------- | -------- | --------------------------------------------------- |
|
|
102
|
-
| `type` | `"string" \| "number" \| "boolean"` | Yes | The value type. |
|
|
103
|
-
| `default` | Matches `type` | No | Default value when the field is not persisted. |
|
|
104
|
-
| `array` | `true` | No | Collect values into an array. |
|
|
105
|
-
| `description` | `string` | No | Human-readable description for tooling. |
|
|
152
|
+
Reads the current effective state, applies the updater function, and atomically persists the result.
|
|
106
153
|
|
|
107
|
-
|
|
154
|
+
```ts
|
|
155
|
+
await store.update((current) => ({
|
|
156
|
+
...current,
|
|
157
|
+
ui: { ...current.ui, theme: "dark" },
|
|
158
|
+
}));
|
|
159
|
+
```
|
|
108
160
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
161
|
+
When no persisted file exists, the updater receives `defaults` as the current value. Calls `validator` before writing.
|
|
162
|
+
|
|
163
|
+
### `store.patch(partial)`
|
|
164
|
+
|
|
165
|
+
Applies a deep partial update to the current state and persists. Only the provided keys are updated; everything else is preserved.
|
|
112
166
|
|
|
113
167
|
```ts
|
|
114
|
-
|
|
115
|
-
dirPath: configDir("my-cli"),
|
|
116
|
-
fields: {
|
|
117
|
-
theme: { type: "string", default: "light" }, // → string
|
|
118
|
-
verbose: { type: "boolean", default: false }, // → boolean
|
|
119
|
-
retries: { type: "number", default: 3 }, // → number
|
|
120
|
-
token: { type: "string" }, // → string | undefined
|
|
121
|
-
tags: { type: "string", array: true, default: [] },// → string[]
|
|
122
|
-
ids: { type: "number", array: true }, // → number[] | undefined
|
|
123
|
-
},
|
|
124
|
-
});
|
|
168
|
+
await store.patch({ ui: { theme: "solarized" } });
|
|
125
169
|
```
|
|
126
170
|
|
|
127
|
-
|
|
171
|
+
Arrays are replaced wholesale (not merged element-by-element). Calls `validator` before writing.
|
|
128
172
|
|
|
129
|
-
|
|
173
|
+
### `store.reset()`
|
|
174
|
+
|
|
175
|
+
Removes the persisted state file. After reset, `read()` returns `defaults`.
|
|
130
176
|
|
|
131
177
|
```ts
|
|
132
|
-
|
|
178
|
+
await store.reset();
|
|
133
179
|
```
|
|
134
180
|
|
|
135
|
-
|
|
181
|
+
Reset is idempotent — calling it when no file exists is a no-op.
|
|
182
|
+
|
|
183
|
+
### Path Helpers
|
|
136
184
|
|
|
137
|
-
|
|
185
|
+
#### `configDir(appName, env?)`
|
|
138
186
|
|
|
139
|
-
|
|
187
|
+
Resolves the platform-standard config directory for the given app name.
|
|
140
188
|
|
|
141
189
|
```ts
|
|
142
|
-
|
|
190
|
+
import { configDir } from "@crustjs/store";
|
|
191
|
+
|
|
192
|
+
const dir = configDir("my-cli");
|
|
193
|
+
// → "/home/user/.config/my-cli" (Linux / macOS)
|
|
194
|
+
// → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
|
|
143
195
|
```
|
|
144
196
|
|
|
145
|
-
|
|
197
|
+
#### `dataDir(appName, env?)`
|
|
146
198
|
|
|
147
|
-
|
|
199
|
+
Resolves the platform-standard data directory.
|
|
148
200
|
|
|
149
201
|
```ts
|
|
150
|
-
|
|
151
|
-
...current,
|
|
152
|
-
theme: "dark",
|
|
153
|
-
}));
|
|
154
|
-
```
|
|
202
|
+
import { dataDir } from "@crustjs/store";
|
|
155
203
|
|
|
156
|
-
|
|
204
|
+
const dir = dataDir("my-cli");
|
|
205
|
+
// → "/home/user/.local/share/my-cli" (Linux / macOS)
|
|
206
|
+
// → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
|
|
207
|
+
```
|
|
157
208
|
|
|
158
|
-
|
|
209
|
+
#### `stateDir(appName, env?)`
|
|
159
210
|
|
|
160
|
-
|
|
211
|
+
Resolves the platform-standard state directory.
|
|
161
212
|
|
|
162
213
|
```ts
|
|
163
|
-
|
|
214
|
+
import { stateDir } from "@crustjs/store";
|
|
215
|
+
|
|
216
|
+
const dir = stateDir("my-cli");
|
|
217
|
+
// → "/home/user/.local/state/my-cli" (Linux / macOS)
|
|
218
|
+
// → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
|
|
164
219
|
```
|
|
165
220
|
|
|
166
|
-
|
|
221
|
+
#### `cacheDir(appName, env?)`
|
|
167
222
|
|
|
168
|
-
|
|
223
|
+
Resolves the platform-standard cache directory.
|
|
169
224
|
|
|
170
|
-
|
|
225
|
+
```ts
|
|
226
|
+
import { cacheDir } from "@crustjs/store";
|
|
171
227
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
228
|
+
const dir = cacheDir("my-cli");
|
|
229
|
+
// → "/home/user/.cache/my-cli" (Linux / macOS)
|
|
230
|
+
// → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
All path helpers accept an optional `PlatformEnv` parameter for deterministic testing without mutating `process.env`.
|
|
177
234
|
|
|
178
|
-
|
|
235
|
+
## Multiple Stores
|
|
179
236
|
|
|
180
237
|
Use the `name` option to create multiple stores under the same directory:
|
|
181
238
|
|
|
@@ -185,11 +242,11 @@ import { createStore, configDir } from "@crustjs/store";
|
|
|
185
242
|
const dir = configDir("my-cli");
|
|
186
243
|
|
|
187
244
|
// Default store → ~/.config/my-cli/config.json
|
|
188
|
-
const
|
|
245
|
+
const settingsStore = createStore({
|
|
189
246
|
dirPath: dir,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
verbose:
|
|
247
|
+
defaults: {
|
|
248
|
+
ui: { theme: "light" as string, fontSize: 14 },
|
|
249
|
+
verbose: false,
|
|
193
250
|
},
|
|
194
251
|
});
|
|
195
252
|
|
|
@@ -197,9 +254,7 @@ const configStore = createStore({
|
|
|
197
254
|
const authStore = createStore({
|
|
198
255
|
dirPath: dir,
|
|
199
256
|
name: "auth",
|
|
200
|
-
|
|
201
|
-
token: { type: "string" },
|
|
202
|
-
},
|
|
257
|
+
defaults: { token: "" as string },
|
|
203
258
|
});
|
|
204
259
|
```
|
|
205
260
|
|
|
@@ -209,45 +264,144 @@ The `name` must not contain path separators or the `.json` extension.
|
|
|
209
264
|
|
|
210
265
|
## Defaults & Merge
|
|
211
266
|
|
|
212
|
-
When a persisted file exists, `read()`
|
|
267
|
+
When a persisted file exists, `read()` deep-merges defaults for any missing keys:
|
|
213
268
|
|
|
214
269
|
```ts
|
|
215
270
|
const store = createStore({
|
|
216
271
|
dirPath: configDir("my-cli"),
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
verbose:
|
|
220
|
-
retries: { type: "number", default: 3 },
|
|
272
|
+
defaults: {
|
|
273
|
+
ui: { theme: "light", fontSize: 14 },
|
|
274
|
+
verbose: false,
|
|
221
275
|
},
|
|
222
276
|
});
|
|
223
277
|
|
|
224
|
-
// Persisted file contains: { "theme": "dark" }
|
|
225
|
-
const
|
|
226
|
-
// → { theme: "dark",
|
|
278
|
+
// Persisted file contains: { "ui": { "theme": "dark" }, "verbose": true }
|
|
279
|
+
const state = await store.read();
|
|
280
|
+
// → { ui: { theme: "dark", fontSize: 14 }, verbose: true }
|
|
227
281
|
```
|
|
228
282
|
|
|
229
|
-
### Merge
|
|
283
|
+
### Merge Rules
|
|
230
284
|
|
|
231
|
-
-
|
|
232
|
-
- Missing
|
|
233
|
-
-
|
|
234
|
-
-
|
|
235
|
-
-
|
|
285
|
+
- Persisted values override defaults at every nesting level.
|
|
286
|
+
- Missing keys fall back to defaults (recursively for nested objects).
|
|
287
|
+
- Arrays are replaced wholesale — not merged element-by-element.
|
|
288
|
+
- Unknown persisted keys are dropped by default (`pruneUnknown: true`). Set `pruneUnknown: false` to preserve them.
|
|
289
|
+
- All values are deep-cloned to prevent shared-reference mutation from defaults.
|
|
236
290
|
- Falsy values (`null`, `0`, `""`, `false`) in the persisted file are preserved — only truly missing keys trigger defaults.
|
|
237
291
|
|
|
238
292
|
### Important: defaults are not auto-persisted
|
|
239
293
|
|
|
240
|
-
Merged defaults exist only in memory. The
|
|
294
|
+
Merged defaults exist only in memory. The persisted file remains unchanged until you explicitly call `write()`, `update()`, or `patch()`.
|
|
295
|
+
|
|
296
|
+
## Validation
|
|
297
|
+
|
|
298
|
+
Add schema validation to enforce config integrity on every read, write, update, and patch. When a `validator` is configured, validation is **strict by default** — invalid config fails loudly.
|
|
299
|
+
|
|
300
|
+
### Using `@crustjs/validate`
|
|
301
|
+
|
|
302
|
+
The easiest way to add validation is with store adapters from `@crustjs/validate`:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { z } from "zod";
|
|
306
|
+
import { storeValidator } from "@crustjs/validate/zod";
|
|
307
|
+
import { createStore, configDir } from "@crustjs/store";
|
|
308
|
+
|
|
309
|
+
const store = createStore({
|
|
310
|
+
dirPath: configDir("my-cli"),
|
|
311
|
+
defaults: { theme: "light" as string, verbose: false },
|
|
312
|
+
validator: storeValidator(
|
|
313
|
+
z.object({
|
|
314
|
+
theme: z.enum(["light", "dark"]),
|
|
315
|
+
verbose: z.boolean(),
|
|
316
|
+
}),
|
|
317
|
+
),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// write() validates before persisting
|
|
321
|
+
await store.write({ theme: "neon", verbose: true });
|
|
322
|
+
// → throws CrustStoreError("VALIDATION")
|
|
323
|
+
|
|
324
|
+
// read() validates after applying defaults
|
|
325
|
+
const config = await store.read();
|
|
326
|
+
// → throws if persisted config is invalid
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`storeValidatorSync()` is also available for synchronous schemas.
|
|
330
|
+
|
|
331
|
+
For Effect schemas, wrap with `Schema.standardSchemaV1()`:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
import * as Schema from "effect/Schema";
|
|
335
|
+
import { storeValidator } from "@crustjs/validate/effect";
|
|
336
|
+
|
|
337
|
+
const store = createStore({
|
|
338
|
+
dirPath: configDir("my-cli"),
|
|
339
|
+
defaults: { theme: "light" as string },
|
|
340
|
+
validator: storeValidator(
|
|
341
|
+
Schema.standardSchemaV1(
|
|
342
|
+
Schema.Struct({ theme: Schema.Literal("light", "dark") }),
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Custom validators
|
|
349
|
+
|
|
350
|
+
You can also provide a validator function directly without `@crustjs/validate`:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import type { StoreValidator } from "@crustjs/store";
|
|
354
|
+
|
|
355
|
+
const validator: StoreValidator<{ theme: string }> = (value) => {
|
|
356
|
+
const config = value as { theme: string };
|
|
357
|
+
if (config.theme !== "light" && config.theme !== "dark") {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
issues: [{ message: 'Must be "light" or "dark"', path: "theme" }],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return { ok: true, value: config };
|
|
364
|
+
};
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The `StoreValidator<T>` contract is `(value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>`, where `StoreValidatorResult<T>` is `{ ok: true, value: T } | { ok: false, issues: StoreValidatorIssue[] }`.
|
|
368
|
+
|
|
369
|
+
### Validation behavior
|
|
370
|
+
|
|
371
|
+
- **Write**: Validates before persisting. If the validator transforms the value, the transformed result is persisted.
|
|
372
|
+
- **Read**: Validates after applying defaults. Invalid persisted config fails loudly — no silent fallback to defaults.
|
|
373
|
+
- **Update**: Reads raw config (no validation), applies updater, validates the result, then persists.
|
|
374
|
+
- **Patch**: Reads raw config, applies deep partial merge, validates the result, then persists.
|
|
375
|
+
- **Reset**: No validation — just removes the persisted file.
|
|
376
|
+
|
|
377
|
+
### Catching validation errors
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
import { CrustStoreError } from "@crustjs/store";
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
await store.read();
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
386
|
+
// err.details is { operation: "read" | "write" | "update" | "patch", issues: StoreValidationIssue[] }
|
|
387
|
+
console.error(`Validation failed during ${err.details.operation}:`);
|
|
388
|
+
for (const issue of err.details.issues) {
|
|
389
|
+
console.error(` ${issue.path || "(root)"}: ${issue.message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
241
394
|
|
|
242
395
|
## Error Handling
|
|
243
396
|
|
|
244
397
|
All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a typed `code` property:
|
|
245
398
|
|
|
246
|
-
| Code
|
|
247
|
-
|
|
|
248
|
-
| `PATH`
|
|
249
|
-
| `PARSE`
|
|
250
|
-
| `IO`
|
|
399
|
+
| Code | When | Details |
|
|
400
|
+
| ------------ | --------------------------------------------------------------- | --------------------------- |
|
|
401
|
+
| `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
|
|
402
|
+
| `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
|
|
403
|
+
| `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
|
|
404
|
+
| `VALIDATION` | Config fails validator on read, write, update, or patch | `{ operation, issues }` |
|
|
251
405
|
|
|
252
406
|
### Catching errors by code
|
|
253
407
|
|
|
@@ -255,12 +409,12 @@ All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a
|
|
|
255
409
|
import { CrustStoreError } from "@crustjs/store";
|
|
256
410
|
|
|
257
411
|
try {
|
|
258
|
-
const
|
|
412
|
+
const state = await store.read();
|
|
259
413
|
} catch (err) {
|
|
260
414
|
if (err instanceof CrustStoreError) {
|
|
261
415
|
switch (err.code) {
|
|
262
416
|
case "PARSE":
|
|
263
|
-
console.error(`Corrupt
|
|
417
|
+
console.error(`Corrupt file at ${err.details.path}`);
|
|
264
418
|
console.error("Delete the file and retry, or fix the JSON manually.");
|
|
265
419
|
break;
|
|
266
420
|
case "IO":
|
|
@@ -287,14 +441,14 @@ if (err instanceof CrustStoreError && err.is("IO")) {
|
|
|
287
441
|
`CrustStoreError` preserves the original error as `cause` for debugging:
|
|
288
442
|
|
|
289
443
|
```ts
|
|
290
|
-
if (err instanceof CrustStoreError && err.is("
|
|
291
|
-
console.error("
|
|
444
|
+
if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
445
|
+
console.error("Validation error:", err.cause);
|
|
292
446
|
}
|
|
293
447
|
```
|
|
294
448
|
|
|
295
449
|
### Malformed JSON
|
|
296
450
|
|
|
297
|
-
`@crustjs/store` does **not** silently recover from malformed JSON. If the persisted
|
|
451
|
+
`@crustjs/store` does **not** silently recover from malformed JSON. If the persisted file contains invalid JSON, `read()` throws a `CrustStoreError` with `PARSE` code immediately — no fallback to defaults.
|
|
298
452
|
|
|
299
453
|
## Types
|
|
300
454
|
|
|
@@ -303,38 +457,47 @@ All types are exported for use in your application:
|
|
|
303
457
|
```ts
|
|
304
458
|
import type {
|
|
305
459
|
CreateStoreOptions,
|
|
306
|
-
FieldDef,
|
|
307
|
-
FieldsDef,
|
|
308
|
-
InferStoreConfig,
|
|
309
460
|
Store,
|
|
310
461
|
StoreUpdater,
|
|
311
|
-
|
|
462
|
+
StoreValidator,
|
|
463
|
+
StoreValidatorResult,
|
|
464
|
+
StoreValidatorSuccess,
|
|
465
|
+
StoreValidatorFailure,
|
|
466
|
+
StoreValidatorIssue,
|
|
312
467
|
StoreErrorCode,
|
|
468
|
+
StoreValidationIssue,
|
|
469
|
+
ValidationErrorDetails,
|
|
470
|
+
PlatformEnv,
|
|
313
471
|
} from "@crustjs/store";
|
|
314
472
|
```
|
|
315
473
|
|
|
316
|
-
| Type
|
|
317
|
-
|
|
|
318
|
-
| `CreateStoreOptions`
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
474
|
+
| Type | Description |
|
|
475
|
+
| ------------------------- | ----------------------------------------------------------------------- |
|
|
476
|
+
| `CreateStoreOptions` | Options object for `createStore()`. |
|
|
477
|
+
| `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`. |
|
|
478
|
+
| `StoreUpdater` | Updater function type `(current: T) => T`. |
|
|
479
|
+
| `StoreValidator` | Validator function contract `(value: unknown) => StoreValidatorResult`. |
|
|
480
|
+
| `StoreValidatorResult` | Discriminated union: `StoreValidatorSuccess \| StoreValidatorFailure`. |
|
|
481
|
+
| `StoreValidatorSuccess` | `{ ok: true, value: T }`. |
|
|
482
|
+
| `StoreValidatorFailure` | `{ ok: false, issues: StoreValidatorIssue[] }`. |
|
|
483
|
+
| `StoreValidatorIssue` | `{ message: string, path: string }`. |
|
|
484
|
+
| `StoreValidationIssue` | Validation issue in error details payload. |
|
|
485
|
+
| `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
|
|
486
|
+
| `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION"`. |
|
|
487
|
+
| `PlatformEnv` | Injectable platform environment for testing path helpers. |
|
|
488
|
+
| `CrustStoreError` | Typed error class with `code`, `details`, and `cause`. |
|
|
489
|
+
|
|
490
|
+
## Scope & Non-Goals
|
|
491
|
+
|
|
492
|
+
`@crustjs/store` is intentionally focused. The following are **not** included:
|
|
493
|
+
|
|
494
|
+
- **Cross-process locking** — assumes single-process usage for write coordination.
|
|
332
495
|
- **Built-in encryption or keychain integration.**
|
|
333
|
-
- **Cross-process locking** — v1 assumes single-process usage for write coordination.
|
|
334
496
|
- **Sync API variants** (`readSync`, `writeSync`, etc.).
|
|
335
|
-
- **
|
|
497
|
+
- **Automatic migration of invalid persisted config** — validation fails loudly; callers handle recovery.
|
|
336
498
|
- **Alternative formats** (YAML, TOML, JSON5) — JSON is the only on-disk format.
|
|
337
|
-
- **
|
|
499
|
+
- **Remote/cloud synchronization** or multi-device sync.
|
|
500
|
+
- **Domain-specific migration frameworks** beyond simple store-level upgrade hooks.
|
|
338
501
|
|
|
339
502
|
## License
|
|
340
503
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,251 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Recursively makes all properties of `T` optional.
|
|
3
|
+
*
|
|
4
|
+
* - Plain objects are recursed into — each nested key becomes optional.
|
|
5
|
+
* - Arrays are left as-is (replaced wholesale, not partially updated).
|
|
6
|
+
* - Primitives pass through unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Used by {@link Store.patch} for deep partial updates.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* type Config = { ui: { theme: string; fontSize: number }; verbose: boolean };
|
|
13
|
+
* type Partial = DeepPartial<Config>;
|
|
14
|
+
* // → { ui?: { theme?: string; fontSize?: number }; verbose?: boolean }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
type DeepPartial<T> = T extends readonly unknown[] ? T : T extends Record<string, unknown> ? { [K in keyof T]? : DeepPartial<T[K]> } : T;
|
|
18
|
+
/**
|
|
19
|
+
* Result returned by a {@link StoreValidator} when validation succeeds.
|
|
20
|
+
*
|
|
21
|
+
* Carries the (possibly transformed) config value from the schema.
|
|
22
|
+
*/
|
|
23
|
+
interface StoreValidatorSuccess<T> {
|
|
24
|
+
readonly ok: true;
|
|
25
|
+
readonly value: T;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A single validation issue reported by a {@link StoreValidator}.
|
|
29
|
+
*
|
|
30
|
+
* Compatible with the canonical `ValidationIssue` shape used across the Crust
|
|
31
|
+
* validation platform.
|
|
32
|
+
*/
|
|
33
|
+
interface StoreValidatorIssue {
|
|
34
|
+
/** Human-readable description of the validation failure. */
|
|
35
|
+
message: string;
|
|
36
|
+
/** Dot-path to the invalid field, or empty string for root-level issues. */
|
|
37
|
+
path: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Result returned by a {@link StoreValidator} when validation fails.
|
|
41
|
+
*
|
|
42
|
+
* Carries structured issues for programmatic handling and error rendering.
|
|
43
|
+
*/
|
|
44
|
+
interface StoreValidatorFailure {
|
|
45
|
+
readonly ok: false;
|
|
46
|
+
readonly issues: readonly StoreValidatorIssue[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Discriminated union result from a {@link StoreValidator}.
|
|
50
|
+
*/
|
|
51
|
+
type StoreValidatorResult<T> = StoreValidatorSuccess<T> | StoreValidatorFailure;
|
|
52
|
+
/**
|
|
53
|
+
* A provider-agnostic validator function for full config objects.
|
|
54
|
+
*
|
|
55
|
+
* Accepts a config value and returns a discriminated result indicating
|
|
56
|
+
* success (with a possibly-transformed value) or failure (with structured issues).
|
|
57
|
+
*
|
|
58
|
+
* Store adapters in `@crustjs/validate` produce functions matching this contract
|
|
59
|
+
* from Standard Schema, Zod, or Effect schemas.
|
|
60
|
+
*
|
|
61
|
+
* @typeParam T - The expected config type.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const validator: StoreValidator<Config> = (config) => {
|
|
66
|
+
* if (typeof config.theme !== "string") {
|
|
67
|
+
* return { ok: false, issues: [{ message: "Expected string", path: "theme" }] };
|
|
68
|
+
* }
|
|
69
|
+
* return { ok: true, value: config };
|
|
70
|
+
* };
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
type StoreValidator<T> = (value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>;
|
|
74
|
+
/**
|
|
75
|
+
* Options for {@link createStore}.
|
|
76
|
+
*
|
|
77
|
+
* @typeParam T - The store data shape, inferred from `defaults`.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const store = createStore({
|
|
82
|
+
* dirPath: configDir("my-cli"),
|
|
83
|
+
* defaults: {
|
|
84
|
+
* ui: { theme: "light", fontSize: 14 },
|
|
85
|
+
* verbose: false,
|
|
86
|
+
* tags: ["default"],
|
|
87
|
+
* },
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
interface CreateStoreOptions<T extends Record<string, unknown>> {
|
|
92
|
+
/**
|
|
93
|
+
* Absolute directory path where the store JSON file is persisted.
|
|
94
|
+
*
|
|
95
|
+
* Use a path helper ({@link configDir}, {@link dataDir}, etc.) to resolve
|
|
96
|
+
* the platform-standard directory from an app name.
|
|
97
|
+
*/
|
|
98
|
+
dirPath: string;
|
|
99
|
+
/**
|
|
100
|
+
* Store name used as the JSON filename in the directory.
|
|
101
|
+
*
|
|
102
|
+
* Defaults to `"config"`, producing `config.json`. Set to a different value
|
|
103
|
+
* to create multiple stores under the same directory
|
|
104
|
+
* (e.g. `name: "auth"` → `auth.json`).
|
|
105
|
+
*
|
|
106
|
+
* Must not contain path separators or the `.json` extension.
|
|
107
|
+
*/
|
|
108
|
+
name?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Default values for the store. Defines the data shape and provides
|
|
111
|
+
* fallback values when no persisted file exists or keys are missing.
|
|
112
|
+
*
|
|
113
|
+
* The TypeScript type of `defaults` determines the store's `T` parameter.
|
|
114
|
+
*/
|
|
115
|
+
defaults: T;
|
|
116
|
+
/**
|
|
117
|
+
* Optional validator function applied to config objects during read, write,
|
|
118
|
+
* update, and patch operations.
|
|
119
|
+
*
|
|
120
|
+
* When provided, validation is **strict by default**: invalid config will
|
|
121
|
+
* cause a `CrustStoreError` with `VALIDATION` code to be thrown on both
|
|
122
|
+
* persistence (write/update/patch) and load (read) paths.
|
|
123
|
+
*
|
|
124
|
+
* Use store adapters from `@crustjs/validate/standard`, `@crustjs/validate/zod`,
|
|
125
|
+
* or `@crustjs/validate/effect` to create validators from schema definitions.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { storeValidator } from "@crustjs/validate/zod";
|
|
130
|
+
* import { z } from "zod";
|
|
131
|
+
*
|
|
132
|
+
* const store = createStore({
|
|
133
|
+
* dirPath: configDir("my-cli"),
|
|
134
|
+
* defaults: { theme: "light", verbose: false },
|
|
135
|
+
* validator: storeValidator(z.object({ theme: z.enum(["light", "dark"]) })),
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
validator?: StoreValidator<T>;
|
|
140
|
+
/**
|
|
141
|
+
* When `true` (the default), persisted keys not present in `defaults`
|
|
142
|
+
* are dropped on read. Set to `false` to preserve unknown keys.
|
|
143
|
+
*
|
|
144
|
+
* @default true
|
|
145
|
+
*/
|
|
146
|
+
pruneUnknown?: boolean;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Receives the current effective state and returns an updated state.
|
|
150
|
+
*
|
|
151
|
+
* Used by {@link Store.update} to apply mutations atomically.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* await store.update((current) => ({
|
|
156
|
+
* ...current,
|
|
157
|
+
* ui: { ...current.ui, theme: "dark" },
|
|
158
|
+
* }));
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
type StoreUpdater<T> = (current: T) => NoInfer<T>;
|
|
162
|
+
/**
|
|
163
|
+
* A typed async store returned by {@link createStore}.
|
|
164
|
+
*
|
|
165
|
+
* Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
|
|
166
|
+
* typed state object persisted as JSON on the local filesystem.
|
|
167
|
+
*
|
|
168
|
+
* @typeParam T - The store data shape, inferred from `defaults`.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* const store = createStore({
|
|
173
|
+
* dirPath: configDir("my-cli"),
|
|
174
|
+
* defaults: {
|
|
175
|
+
* ui: { theme: "light", fontSize: 14 },
|
|
176
|
+
* verbose: false,
|
|
177
|
+
* },
|
|
178
|
+
* });
|
|
179
|
+
*
|
|
180
|
+
* const state = await store.read();
|
|
181
|
+
* await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
|
|
182
|
+
* await store.update((s) => ({ ...s, verbose: false }));
|
|
183
|
+
* await store.patch({ ui: { theme: "dark" } });
|
|
184
|
+
* await store.reset();
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
interface Store<T> {
|
|
188
|
+
/**
|
|
189
|
+
* Reads the persisted state, merging defaults for missing keys.
|
|
190
|
+
*
|
|
191
|
+
* Always returns a complete `T` — missing persisted keys are filled
|
|
192
|
+
* from `defaults`.
|
|
193
|
+
*
|
|
194
|
+
* When a validator is configured, the merged config is validated after
|
|
195
|
+
* defaults are applied. Invalid persisted config fails loudly.
|
|
196
|
+
*
|
|
197
|
+
* @returns The effective config value (possibly transformed by the validator).
|
|
198
|
+
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
199
|
+
* @throws {CrustStoreError} `VALIDATION` if the config fails validation.
|
|
200
|
+
* @throws {CrustStoreError} `IO` on filesystem read failures.
|
|
201
|
+
*/
|
|
202
|
+
read(): Promise<T>;
|
|
203
|
+
/**
|
|
204
|
+
* Atomically persists the full state object.
|
|
205
|
+
*
|
|
206
|
+
* When a validator is configured, the config is validated before persistence.
|
|
207
|
+
*
|
|
208
|
+
* @param state - The complete state to persist.
|
|
209
|
+
* @throws {CrustStoreError} `VALIDATION` if the config fails validation.
|
|
210
|
+
* @throws {CrustStoreError} `IO` on filesystem write failures.
|
|
211
|
+
*/
|
|
212
|
+
write(state: NoInfer<T>): Promise<void>;
|
|
213
|
+
/**
|
|
214
|
+
* Reads current effective state, applies the updater, and persists.
|
|
215
|
+
*
|
|
216
|
+
* When a validator is configured, the updated config is validated before
|
|
217
|
+
* persistence. The read step during update does **not** validate separately
|
|
218
|
+
* to avoid double-validation overhead — the `current` value passed to the
|
|
219
|
+
* updater may not have been validated (e.g. if the persisted file was
|
|
220
|
+
* manually edited).
|
|
221
|
+
*
|
|
222
|
+
* @param updater - Function receiving current state and returning updated state.
|
|
223
|
+
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
224
|
+
* @throws {CrustStoreError} `VALIDATION` if the updated config fails validation.
|
|
225
|
+
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
226
|
+
*/
|
|
227
|
+
update(updater: StoreUpdater<T>): Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* Applies a deep partial update to the current state and persists.
|
|
230
|
+
*
|
|
231
|
+
* Only the provided keys are updated; everything else is preserved.
|
|
232
|
+
* Arrays are replaced wholesale (not merged element-by-element).
|
|
233
|
+
*
|
|
234
|
+
* @param partial - A deep-partial subset of the state to merge in.
|
|
235
|
+
* @throws {CrustStoreError} `VALIDATION` if the merged config fails validation.
|
|
236
|
+
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
237
|
+
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
238
|
+
*/
|
|
239
|
+
patch(partial: DeepPartial<NoInfer<T>>): Promise<void>;
|
|
240
|
+
/**
|
|
241
|
+
* Removes the persisted state file, returning the store to
|
|
242
|
+
* defaults-on-read behavior.
|
|
243
|
+
*
|
|
244
|
+
* @throws {CrustStoreError} `IO` on filesystem deletion failures.
|
|
245
|
+
*/
|
|
246
|
+
reset(): Promise<void>;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
2
249
|
* Contextual details attached to a `PATH` error.
|
|
3
250
|
*
|
|
4
251
|
* Returned when the resolved or provided config file path is invalid.
|
|
@@ -28,6 +275,18 @@ interface IOErrorDetails {
|
|
|
28
275
|
operation: "read" | "write" | "delete";
|
|
29
276
|
}
|
|
30
277
|
/**
|
|
278
|
+
* Contextual details attached to a `VALIDATION` error.
|
|
279
|
+
*
|
|
280
|
+
* Returned when a store validator rejects a config object during
|
|
281
|
+
* read, write, update, or patch operations.
|
|
282
|
+
*/
|
|
283
|
+
interface ValidationErrorDetails {
|
|
284
|
+
/** The store operation during which validation failed. */
|
|
285
|
+
operation: "read" | "write" | "update" | "patch";
|
|
286
|
+
/** Structured validation issues for programmatic handling. */
|
|
287
|
+
issues: readonly StoreValidatorIssue[];
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
31
290
|
* Maps each {@link StoreErrorCode} to its structured details type.
|
|
32
291
|
*
|
|
33
292
|
* Used internally for conditional constructor parameters and type narrowing.
|
|
@@ -36,6 +295,7 @@ interface StoreErrorDetailsMap {
|
|
|
36
295
|
PATH: PathErrorDetails;
|
|
37
296
|
PARSE: ParseErrorDetails;
|
|
38
297
|
IO: IOErrorDetails;
|
|
298
|
+
VALIDATION: ValidationErrorDetails;
|
|
39
299
|
}
|
|
40
300
|
/**
|
|
41
301
|
* All possible error codes emitted by `@crustjs/store`.
|
|
@@ -43,6 +303,7 @@ interface StoreErrorDetailsMap {
|
|
|
43
303
|
* - `PATH` — Invalid or unsupported config file path
|
|
44
304
|
* - `PARSE` — Malformed JSON in persisted config file
|
|
45
305
|
* - `IO` — Filesystem read, write, or delete failure
|
|
306
|
+
* - `VALIDATION` — Config object rejected by a store validator
|
|
46
307
|
*
|
|
47
308
|
* @example
|
|
48
309
|
* ```ts
|
|
@@ -56,6 +317,11 @@ interface StoreErrorDetailsMap {
|
|
|
56
317
|
* case "PARSE":
|
|
57
318
|
* console.error(`Corrupt config at ${err.details.path}`);
|
|
58
319
|
* break;
|
|
320
|
+
* case "VALIDATION":
|
|
321
|
+
* for (const issue of err.details.issues) {
|
|
322
|
+
* console.error(`${issue.path}: ${issue.message}`);
|
|
323
|
+
* }
|
|
324
|
+
* break;
|
|
59
325
|
* case "IO":
|
|
60
326
|
* console.error(`File ${err.details.operation} failed: ${err.message}`);
|
|
61
327
|
* break;
|
|
@@ -68,7 +334,7 @@ type StoreErrorCode = keyof StoreErrorDetailsMap;
|
|
|
68
334
|
/** Resolves the details type for a given {@link StoreErrorCode}. */
|
|
69
335
|
type StoreErrorDetails<C extends StoreErrorCode> = StoreErrorDetailsMap[C];
|
|
70
336
|
/**
|
|
71
|
-
* A typed error thrown by `@crustjs/store` for path, parse, and
|
|
337
|
+
* A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
|
|
72
338
|
*
|
|
73
339
|
* Every `CrustStoreError` carries a {@link StoreErrorCode} that identifies the failure
|
|
74
340
|
* category, along with structured {@link StoreErrorDetails} for programmatic handling
|
|
@@ -143,10 +409,11 @@ interface PlatformEnv {
|
|
|
143
409
|
* Resolves the platform-standard config directory for the given app.
|
|
144
410
|
*
|
|
145
411
|
* Platform conventions:
|
|
146
|
-
* - **Linux**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
|
|
147
|
-
* - **macOS**: `~/Library/Application Support/<appName>`
|
|
412
|
+
* - **Linux / macOS**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
|
|
148
413
|
* - **Windows**: `%APPDATA%/<appName>` or `~/AppData/Roaming/<appName>`
|
|
149
414
|
*
|
|
415
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
416
|
+
*
|
|
150
417
|
* @param appName - Application name used as directory name. Must be a non-empty
|
|
151
418
|
* string without path separators.
|
|
152
419
|
* @param env - Optional platform environment override for testing.
|
|
@@ -159,241 +426,99 @@ interface PlatformEnv {
|
|
|
159
426
|
*
|
|
160
427
|
* const dir = configDir("my-cli");
|
|
161
428
|
* // → "/home/user/.config/my-cli" (Linux)
|
|
162
|
-
* // → "/Users/user/
|
|
429
|
+
* // → "/Users/user/.config/my-cli" (macOS)
|
|
163
430
|
* // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
|
|
164
431
|
* ```
|
|
165
432
|
*/
|
|
166
433
|
declare function configDir(appName: string, env?: PlatformEnv): string;
|
|
167
|
-
/** Supported type literals for store fields. */
|
|
168
|
-
type ValueType = "string" | "number" | "boolean";
|
|
169
|
-
/**
|
|
170
|
-
* Resolves a type literal to its corresponding TypeScript primitive type.
|
|
171
|
-
*
|
|
172
|
-
* - `"string"` → `string`
|
|
173
|
-
* - `"number"` → `number`
|
|
174
|
-
* - `"boolean"` → `boolean`
|
|
175
|
-
*/
|
|
176
|
-
type ResolvePrimitive<T extends ValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
|
|
177
|
-
/** Shared fields present on every store field definition. */
|
|
178
|
-
interface FieldDefBase {
|
|
179
|
-
/** Human-readable description for documentation and tooling. */
|
|
180
|
-
description?: string;
|
|
181
|
-
}
|
|
182
|
-
/** Base for scalar (non-array) fields — `array` must be omitted. */
|
|
183
|
-
interface ScalarFieldBase extends FieldDefBase {
|
|
184
|
-
/** Must be omitted for scalar fields — set to `true` for array fields. */
|
|
185
|
-
array?: never;
|
|
186
|
-
}
|
|
187
|
-
/** A scalar string field. */
|
|
188
|
-
interface StringFieldDef extends ScalarFieldBase {
|
|
189
|
-
type: "string";
|
|
190
|
-
/** Default string value when the field is not persisted. */
|
|
191
|
-
default?: string;
|
|
192
|
-
}
|
|
193
|
-
/** A scalar number field. */
|
|
194
|
-
interface NumberFieldDef extends ScalarFieldBase {
|
|
195
|
-
type: "number";
|
|
196
|
-
/** Default number value when the field is not persisted. */
|
|
197
|
-
default?: number;
|
|
198
|
-
}
|
|
199
|
-
/** A scalar boolean field. */
|
|
200
|
-
interface BooleanFieldDef extends ScalarFieldBase {
|
|
201
|
-
type: "boolean";
|
|
202
|
-
/** Default boolean value when the field is not persisted. */
|
|
203
|
-
default?: boolean;
|
|
204
|
-
}
|
|
205
|
-
/** Base for array fields — `array` is required as `true`. */
|
|
206
|
-
interface ArrayFieldBase extends FieldDefBase {
|
|
207
|
-
/** Collect values into an array. */
|
|
208
|
-
array: true;
|
|
209
|
-
}
|
|
210
|
-
/** An array of strings field. */
|
|
211
|
-
interface StringArrayFieldDef extends ArrayFieldBase {
|
|
212
|
-
type: "string";
|
|
213
|
-
/** Default string array value when the field is not persisted. */
|
|
214
|
-
default?: readonly string[];
|
|
215
|
-
}
|
|
216
|
-
/** An array of numbers field. */
|
|
217
|
-
interface NumberArrayFieldDef extends ArrayFieldBase {
|
|
218
|
-
type: "number";
|
|
219
|
-
/** Default number array value when the field is not persisted. */
|
|
220
|
-
default?: readonly number[];
|
|
221
|
-
}
|
|
222
|
-
/** An array of booleans field. */
|
|
223
|
-
interface BooleanArrayFieldDef extends ArrayFieldBase {
|
|
224
|
-
type: "boolean";
|
|
225
|
-
/** Default boolean array value when the field is not persisted. */
|
|
226
|
-
default?: readonly boolean[];
|
|
227
|
-
}
|
|
228
434
|
/**
|
|
229
|
-
*
|
|
435
|
+
* Resolves the platform-standard data directory for the given app.
|
|
230
436
|
*
|
|
231
|
-
*
|
|
437
|
+
* Platform conventions:
|
|
438
|
+
* - **Linux / macOS**: `$XDG_DATA_HOME/<appName>` or `~/.local/share/<appName>`
|
|
439
|
+
* - **Windows**: `%LOCALAPPDATA%/<appName>/Data` or `~/AppData/Local/<appName>/Data`
|
|
232
440
|
*
|
|
233
|
-
*
|
|
234
|
-
* ```ts
|
|
235
|
-
* const fields = {
|
|
236
|
-
* theme: { type: "string", default: "light" },
|
|
237
|
-
* verbose: { type: "boolean", default: false },
|
|
238
|
-
* tags: { type: "string", array: true, default: [] },
|
|
239
|
-
* token: { type: "string" }, // optional — no default
|
|
240
|
-
* } satisfies FieldsDef;
|
|
241
|
-
* ```
|
|
242
|
-
*/
|
|
243
|
-
type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef;
|
|
244
|
-
/** Record mapping field names to their definitions. */
|
|
245
|
-
type FieldsDef = Record<string, FieldDef>;
|
|
246
|
-
/**
|
|
247
|
-
* Infer the resolved type for a single field definition.
|
|
441
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
248
442
|
*
|
|
249
|
-
* -
|
|
250
|
-
*
|
|
251
|
-
* -
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
array: true;
|
|
255
|
-
} ? F extends {
|
|
256
|
-
default: readonly ResolvePrimitive<F["type"]>[];
|
|
257
|
-
} ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
|
|
258
|
-
default: ResolvePrimitive<F["type"]>;
|
|
259
|
-
} ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined;
|
|
260
|
-
/**
|
|
261
|
-
* Maps a full {@link FieldsDef} record to the inferred config object type.
|
|
443
|
+
* @param appName - Application name used as directory name. Must be a non-empty
|
|
444
|
+
* string without path separators.
|
|
445
|
+
* @param env - Optional platform environment override for testing.
|
|
446
|
+
* @returns Absolute path to the app's data directory.
|
|
447
|
+
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
262
448
|
*
|
|
263
449
|
* @example
|
|
264
450
|
* ```ts
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* // →
|
|
451
|
+
* import { dataDir } from "@crustjs/store";
|
|
452
|
+
*
|
|
453
|
+
* const dir = dataDir("my-cli");
|
|
454
|
+
* // → "/home/user/.local/share/my-cli" (Linux)
|
|
455
|
+
* // → "/Users/user/.local/share/my-cli" (macOS)
|
|
456
|
+
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
|
|
270
457
|
* ```
|
|
271
458
|
*/
|
|
272
|
-
|
|
459
|
+
declare function dataDir(appName: string, env?: PlatformEnv): string;
|
|
273
460
|
/**
|
|
274
|
-
*
|
|
461
|
+
* Resolves the platform-standard state directory for the given app.
|
|
275
462
|
*
|
|
276
|
-
*
|
|
463
|
+
* Platform conventions:
|
|
464
|
+
* - **Linux / macOS**: `$XDG_STATE_HOME/<appName>` or `~/.local/state/<appName>`
|
|
465
|
+
* - **Windows**: `%LOCALAPPDATA%/<appName>/State` or `~/AppData/Local/<appName>/State`
|
|
277
466
|
*
|
|
278
|
-
*
|
|
279
|
-
* ```ts
|
|
280
|
-
* const store = createStore({
|
|
281
|
-
* dirPath: configDir("my-cli"),
|
|
282
|
-
* fields: {
|
|
283
|
-
* theme: { type: "string", default: "light" },
|
|
284
|
-
* verbose: { type: "boolean", default: false },
|
|
285
|
-
* },
|
|
286
|
-
* });
|
|
287
|
-
* ```
|
|
288
|
-
*/
|
|
289
|
-
interface CreateStoreOptions<F extends FieldsDef> {
|
|
290
|
-
/**
|
|
291
|
-
* Absolute directory path where the store JSON file is persisted.
|
|
292
|
-
*
|
|
293
|
-
* Use the {@link configDir} helper to resolve the platform-standard
|
|
294
|
-
* config directory from an app name.
|
|
295
|
-
*/
|
|
296
|
-
dirPath: string;
|
|
297
|
-
/**
|
|
298
|
-
* Store name used as the JSON filename in the directory.
|
|
299
|
-
*
|
|
300
|
-
* Defaults to `"config"`, producing `config.json`. Set to a different value
|
|
301
|
-
* to create multiple stores under the same directory
|
|
302
|
-
* (e.g. `name: "auth"` → `auth.json`).
|
|
303
|
-
*
|
|
304
|
-
* Must not contain path separators or the `.json` extension.
|
|
305
|
-
*/
|
|
306
|
-
name?: string;
|
|
307
|
-
/**
|
|
308
|
-
* Field definitions that declare the store's config schema.
|
|
309
|
-
*
|
|
310
|
-
* Each key maps to a {@link FieldDef} with a `type` discriminant and optional
|
|
311
|
-
* `default`. Fields without a `default` are optional (`T | undefined`).
|
|
312
|
-
*/
|
|
313
|
-
fields: F;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Receives the current effective config and returns an updated config.
|
|
467
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
317
468
|
*
|
|
318
|
-
*
|
|
469
|
+
* @param appName - Application name used as directory name. Must be a non-empty
|
|
470
|
+
* string without path separators.
|
|
471
|
+
* @param env - Optional platform environment override for testing.
|
|
472
|
+
* @returns Absolute path to the app's state directory.
|
|
473
|
+
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
319
474
|
*
|
|
320
475
|
* @example
|
|
321
476
|
* ```ts
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
477
|
+
* import { stateDir } from "@crustjs/store";
|
|
478
|
+
*
|
|
479
|
+
* const dir = stateDir("my-cli");
|
|
480
|
+
* // → "/home/user/.local/state/my-cli" (Linux)
|
|
481
|
+
* // → "/Users/user/.local/state/my-cli" (macOS)
|
|
482
|
+
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
|
|
326
483
|
* ```
|
|
327
484
|
*/
|
|
328
|
-
|
|
485
|
+
declare function stateDir(appName: string, env?: PlatformEnv): string;
|
|
329
486
|
/**
|
|
330
|
-
*
|
|
487
|
+
* Resolves the platform-standard cache directory for the given app.
|
|
331
488
|
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
489
|
+
* Platform conventions:
|
|
490
|
+
* - **Linux / macOS**: `$XDG_CACHE_HOME/<appName>` or `~/.cache/<appName>`
|
|
491
|
+
* - **Windows**: `%LOCALAPPDATA%/<appName>/Cache` or `~/AppData/Local/<appName>/Cache`
|
|
334
492
|
*
|
|
335
|
-
*
|
|
493
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
494
|
+
*
|
|
495
|
+
* @param appName - Application name used as directory name. Must be a non-empty
|
|
496
|
+
* string without path separators.
|
|
497
|
+
* @param env - Optional platform environment override for testing.
|
|
498
|
+
* @returns Absolute path to the app's cache directory.
|
|
499
|
+
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
336
500
|
*
|
|
337
501
|
* @example
|
|
338
502
|
* ```ts
|
|
339
|
-
*
|
|
340
|
-
* dirPath: configDir("my-cli"),
|
|
341
|
-
* fields: {
|
|
342
|
-
* theme: { type: "string", default: "light" },
|
|
343
|
-
* },
|
|
344
|
-
* });
|
|
503
|
+
* import { cacheDir } from "@crustjs/store";
|
|
345
504
|
*
|
|
346
|
-
* const
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
505
|
+
* const dir = cacheDir("my-cli");
|
|
506
|
+
* // → "/home/user/.cache/my-cli" (Linux)
|
|
507
|
+
* // → "/Users/user/.cache/my-cli" (macOS)
|
|
508
|
+
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
|
|
350
509
|
* ```
|
|
351
510
|
*/
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Reads the persisted config, applying field defaults for missing keys.
|
|
355
|
-
*
|
|
356
|
-
* Always returns a value — fields with defaults are guaranteed present,
|
|
357
|
-
* fields without defaults may be `undefined`.
|
|
358
|
-
*
|
|
359
|
-
* @returns The effective config value.
|
|
360
|
-
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
361
|
-
* @throws {CrustStoreError} `IO` on filesystem read failures.
|
|
362
|
-
*/
|
|
363
|
-
read(): Promise<TConfig>;
|
|
364
|
-
/**
|
|
365
|
-
* Atomically persists the full config object.
|
|
366
|
-
*
|
|
367
|
-
* @param config - The complete config to persist.
|
|
368
|
-
* @throws {CrustStoreError} `IO` on filesystem write failures.
|
|
369
|
-
*/
|
|
370
|
-
write(config: NoInfer<TConfig>): Promise<void>;
|
|
371
|
-
/**
|
|
372
|
-
* Reads current effective config, applies the updater, and persists.
|
|
373
|
-
*
|
|
374
|
-
* @param updater - Function receiving current config and returning updated config.
|
|
375
|
-
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
376
|
-
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
377
|
-
*/
|
|
378
|
-
update(updater: StoreUpdater<TConfig>): Promise<void>;
|
|
379
|
-
/**
|
|
380
|
-
* Removes the persisted config file, returning the store to defaults-on-read behavior.
|
|
381
|
-
*
|
|
382
|
-
* @throws {CrustStoreError} `IO` on filesystem deletion failures.
|
|
383
|
-
*/
|
|
384
|
-
reset(): Promise<void>;
|
|
385
|
-
}
|
|
511
|
+
declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
386
512
|
/**
|
|
387
|
-
* Creates a typed async
|
|
513
|
+
* Creates a typed async store backed by a local JSON file.
|
|
388
514
|
*
|
|
389
515
|
* The store resolves its file path once at creation time from `dirPath` and
|
|
390
|
-
* optional `name`.
|
|
391
|
-
*
|
|
392
|
-
* determines whether the field is guaranteed or optional (`T | undefined`).
|
|
516
|
+
* optional `name`. The `defaults` object declares the data shape — its
|
|
517
|
+
* TypeScript type determines the store's type parameter `T`.
|
|
393
518
|
*
|
|
394
|
-
* @typeParam
|
|
519
|
+
* @typeParam T - The store data shape (inferred from `defaults`).
|
|
395
520
|
* @param options - Store configuration options.
|
|
396
|
-
* @returns A {@link Store} instance with `read`, `write`, `update`, and `reset` methods.
|
|
521
|
+
* @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
|
|
397
522
|
* @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
|
|
398
523
|
*
|
|
399
524
|
* @example
|
|
@@ -402,20 +527,20 @@ interface Store<TConfig> {
|
|
|
402
527
|
*
|
|
403
528
|
* const store = createStore({
|
|
404
529
|
* dirPath: configDir("my-cli"),
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
* verbose:
|
|
408
|
-
* token: { type: "string" },
|
|
530
|
+
* defaults: {
|
|
531
|
+
* ui: { theme: "light", fontSize: 14 },
|
|
532
|
+
* verbose: false,
|
|
409
533
|
* },
|
|
410
534
|
* });
|
|
411
535
|
*
|
|
412
|
-
* const
|
|
413
|
-
* // → { theme: "light",
|
|
536
|
+
* const state = await store.read();
|
|
537
|
+
* // → { ui: { theme: "light", fontSize: 14 }, verbose: false }
|
|
414
538
|
*
|
|
415
|
-
* await store.write({ theme: "dark",
|
|
416
|
-
* await store.update((
|
|
539
|
+
* await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
|
|
540
|
+
* await store.update((s) => ({ ...s, verbose: false }));
|
|
541
|
+
* await store.patch({ ui: { theme: "dark" } });
|
|
417
542
|
* await store.reset();
|
|
418
543
|
* ```
|
|
419
544
|
*/
|
|
420
|
-
declare function createStore<const
|
|
421
|
-
export { createStore, configDir,
|
|
545
|
+
declare function createStore<const T extends Record<string, unknown>>(options: CreateStoreOptions<T>): Store<T>;
|
|
546
|
+
export { stateDir, dataDir, createStore, configDir, cacheDir, ValidationErrorDetails, StoreValidatorSuccess, StoreValidatorResult, StoreValidatorIssue, StoreValidatorFailure, StoreValidator, StoreUpdater, StoreErrorCode, Store, PlatformEnv, CrustStoreError, CreateStoreOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
class
|
|
2
|
+
class K extends Error{code;details;cause;constructor(q,B,z){super(B);this.name="CrustStoreError",this.code=q,this.details=z}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{homedir as f}from"os";import{join as Q}from"path";var P="config";function _(){return{platform:process.platform,env:process.env,homedir:f()}}function T(q){if(!q||q.trim().length===0)throw new K("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new K("PATH","appName must not contain path separators",{path:q})}function h(q){if(!q||q.trim().length===0)throw new K("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new K("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new K("PATH","name must not include the .json extension",{path:q})}function v(q){if(!q||q.trim().length===0)throw new K("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new K("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new K("PATH","dirPath must be a directory path, not a file path (should not end with .json)",{path:q})}function F(q,B,z,G){let H=q.env[B],W=H&&H.trim().length>0?H:Q(q.homedir,...z);return Q(W,G)}function O(q){throw new K("PATH",`Unsupported platform: ${q}`,{path:q})}function m(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let G=z.env.APPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Roaming");return Q(H,q)}default:O(z.platform)}}function u(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let G=z.env.LOCALAPPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Local");return Q(H,q,"Data")}default:O(z.platform)}}function d(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let G=z.env.LOCALAPPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Local");return Q(H,q,"State")}default:O(z.platform)}}function o(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let G=z.env.LOCALAPPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Local");return Q(H,q,"Cache")}default:O(z.platform)}}function b(q,B){v(q);let z=B??P;if(B!==void 0)h(B);return Q(q,`${z}.json`)}function V(q){return q!==null&&typeof q==="object"&&!Array.isArray(q)}function x(q){if(Array.isArray(q))return[...q];if(V(q)){let B={};for(let[z,G]of Object.entries(q))B[z]=x(G);return B}return q}function J(q,B,z=!0){let G={};for(let[H,W]of Object.entries(B))if(q!==void 0&&H in q){let X=q[H];if(V(W)&&V(X))G[H]=J(X,W,z);else G[H]=x(X)}else G[H]=x(W);if(!z&&q!==void 0){for(let[H,W]of Object.entries(q))if(!(H in B))G[H]=x(W)}return G}import{randomUUID as s}from"crypto";import{mkdir as i,readFile as p,rename as n,rm as l,unlink as t,writeFile as r}from"fs/promises";import{dirname as a,join as e}from"path";async function j(q){let B;try{B=await p(q,"utf-8")}catch(z){if(D(z))return;throw new K("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(z)}try{return JSON.parse(B)}catch(z){throw new K("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(z)}}async function R(q,B){let z=a(q),G=e(z,`.config-${s()}.tmp`);try{await i(z,{recursive:!0})}catch(H){throw new K("IO",`Failed to create config directory: ${z}`,{path:q,operation:"write"}).withCause(H)}try{let H=JSON.stringify(B,null,"\t");await r(G,H,"utf-8")}catch(H){throw await U(G),new K("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(H)}try{await n(G,q)}catch(H){throw await U(G),new K("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(H)}}async function A(q){try{await t(q)}catch(B){if(D(B))return;throw new K("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(B)}}function D(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function U(q){try{await l(q,{force:!0})}catch{}}function qq(q){let{dirPath:B,name:z,defaults:G,validator:H,pruneUnknown:W}=q,X=b(B,z),C=W??!0;async function M(L,Y){if(!H)return L;let Z=await H(L);if(Z.ok)return Z.value;let I=Z.issues.map(($)=>({message:$.message,path:$.path})),y=I.map(($)=>$.path?` - ${$.path}: ${$.message}`:` - ${$.message}`),N=y.length>0?`Store validation failed (${Y})
|
|
3
|
+
${y.join(`
|
|
4
|
+
`)}`:`Store validation failed (${Y})`;throw new K("VALIDATION",N,{operation:Y,issues:I})}async function S(){let L=await j(X);return J(L,G,C)}async function k(){let L=await S();return M(L,"read")}async function g(L){let Y=await M(L,"write");await R(X,Y)}async function c(L){let Y=await S(),Z=L(Y),I=await M(Z,"update");await R(X,I)}async function w(L){let Y=await S(),Z=J(L,Y,!1),I=await M(Z,"patch");await R(X,I)}async function E(){await A(X)}return{read:k,write:g,update:c,patch:w,reset:E}}export{d as stateDir,u as dataDir,qq as createStore,m as configDir,o as cacheDir,K as CrustStoreError};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/store",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "DX-first, typed persistence for CLI apps with config/data/state/cache separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "chenxin-yan",
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
"keywords": [
|
|
18
18
|
"cli",
|
|
19
19
|
"config",
|
|
20
|
+
"data",
|
|
21
|
+
"state",
|
|
22
|
+
"cache",
|
|
20
23
|
"store",
|
|
21
24
|
"persistence",
|
|
25
|
+
"xdg",
|
|
22
26
|
"bun",
|
|
23
27
|
"typescript"
|
|
24
28
|
],
|