@crustjs/store 0.4.0 → 0.4.2
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/dist/index.d.ts +3 -81
- package/dist/index.js +27 -110
- package/package.json +68 -68
package/dist/index.d.ts
CHANGED
|
@@ -1,84 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
interface JsonObject {
|
|
5
|
-
[key: string]: JsonValue;
|
|
6
|
-
}
|
|
7
|
-
/** Preserve a type when all of its properties are recursively JSON-compatible. */
|
|
8
|
-
type JsonCompatible<T> = T extends JsonValue ? T : T extends ((...arguments_: never[]) => void) ? never : T extends readonly unknown[] ? { [K in keyof T]: JsonCompatible<T[K]>; } : T extends object ? { [K in keyof T]: JsonCompatible<T[K]>; } : never;
|
|
9
|
-
//#endregion
|
|
10
|
-
//#region ../utils/src/primitive.d.ts
|
|
11
|
-
/** Supported primitive type literals shared by Crust packages. */
|
|
12
|
-
type BaseValueType = "string" | "number" | "boolean";
|
|
13
|
-
/**
|
|
14
|
-
* Resolves a primitive type literal to its corresponding TypeScript type.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* type Port = ResolvePrimitive<"number">;
|
|
19
|
-
* // ^? number
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
type ResolvePrimitive<T extends BaseValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region ../utils/src/schema.d.ts
|
|
25
|
-
/** The Standard Typed types interface. */
|
|
26
|
-
interface StandardSchemaTypes<Input = unknown, Output = Input> {
|
|
27
|
-
/** The input type of the schema. */
|
|
28
|
-
readonly input: Input;
|
|
29
|
-
/** The output type of the schema. */
|
|
30
|
-
readonly output: Output;
|
|
31
|
-
}
|
|
32
|
-
/** The result interface if validation succeeds. */
|
|
33
|
-
interface StandardSchemaSuccessResult<Output> {
|
|
34
|
-
/** The typed output value. */
|
|
35
|
-
readonly value: Output;
|
|
36
|
-
/** A falsy value for `issues` indicates success. */
|
|
37
|
-
readonly issues?: undefined;
|
|
38
|
-
}
|
|
39
|
-
/** The result interface if validation fails. */
|
|
40
|
-
interface StandardSchemaFailureResult {
|
|
41
|
-
/** The issues of failed validation. */
|
|
42
|
-
readonly issues: ReadonlyArray<StandardSchemaIssue>;
|
|
43
|
-
}
|
|
44
|
-
/** The result interface of the validate function. */
|
|
45
|
-
type StandardSchemaResult<Output> = StandardSchemaSuccessResult<Output> | StandardSchemaFailureResult;
|
|
46
|
-
/** The issue interface of the failure output. */
|
|
47
|
-
interface StandardSchemaIssue {
|
|
48
|
-
/** The error message of the issue. */
|
|
49
|
-
readonly message: string;
|
|
50
|
-
/** The path of the issue, if any. */
|
|
51
|
-
readonly path?: ReadonlyArray<PropertyKey | StandardSchemaPathSegment> | undefined;
|
|
52
|
-
}
|
|
53
|
-
/** The path segment interface of the issue. */
|
|
54
|
-
interface StandardSchemaPathSegment {
|
|
55
|
-
/** The key representing a path segment. */
|
|
56
|
-
readonly key: PropertyKey;
|
|
57
|
-
}
|
|
58
|
-
/** The Standard Schema properties interface. */
|
|
59
|
-
interface StandardSchemaProps<Input = unknown, Output = Input> {
|
|
60
|
-
/** The version number of the standard. */
|
|
61
|
-
readonly version: 1;
|
|
62
|
-
/** The vendor name of the schema library. */
|
|
63
|
-
readonly vendor: string;
|
|
64
|
-
/** Inferred types associated with the schema. */
|
|
65
|
-
readonly types?: StandardSchemaTypes<Input, Output> | undefined;
|
|
66
|
-
/** Validates unknown input values. */
|
|
67
|
-
readonly validate: (value: StandardSchemaTypes["input"]) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* A Standard Schema-compatible schema object.
|
|
71
|
-
*
|
|
72
|
-
* Any schema library implementing the Standard Schema v1 spec
|
|
73
|
-
* (Zod, Effect, Valibot, ArkType, etc.) produces objects matching this type.
|
|
74
|
-
*/
|
|
75
|
-
type StandardSchema<Input = unknown, Output = Input> = {
|
|
76
|
-
/** The Standard Schema properties. */
|
|
77
|
-
readonly "~standard": StandardSchemaProps<Input, Output>;
|
|
78
|
-
};
|
|
79
|
-
/** Infer the output type produced by a Standard Schema on success. */
|
|
80
|
-
type InferOutput<S extends StandardSchema> = NonNullable<S["~standard"]["types"]>["output"];
|
|
81
|
-
//#endregion
|
|
1
|
+
import { JsonCompatible } from "@crustjs/utils/json";
|
|
2
|
+
import { BaseValueType, ResolvePrimitive } from "@crustjs/utils/primitive";
|
|
3
|
+
import { InferOutput, StandardSchema } from "@crustjs/utils/schema";
|
|
82
4
|
//#region src/types.d.ts
|
|
83
5
|
/** Supported type literals for store fields. */
|
|
84
6
|
type ValueType = BaseValueType;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { dirname, join, win32 } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { isJsonObject } from "@crustjs/utils/json";
|
|
5
|
+
import { coerceBooleanString, tryCoerceNumber } from "@crustjs/utils/primitive";
|
|
6
|
+
import { normalizeStandardIssues } from "@crustjs/utils/schema";
|
|
4
7
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { isErrnoException } from "@crustjs/utils/error";
|
|
5
9
|
//#region src/errors.ts
|
|
6
10
|
/**
|
|
7
11
|
* A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
|
|
@@ -249,109 +253,24 @@ function resolveStorePath(dirPath, name) {
|
|
|
249
253
|
return join(dirPath, `${name}.json`);
|
|
250
254
|
}
|
|
251
255
|
//#endregion
|
|
252
|
-
//#region
|
|
253
|
-
/** Narrow a JSON document root to an object (not array/scalar/null). */
|
|
254
|
-
function isJsonObject(value) {
|
|
255
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
256
|
-
}
|
|
257
|
-
//#endregion
|
|
258
|
-
//#region ../utils/src/primitive.ts
|
|
259
|
-
/**
|
|
260
|
-
* Attempts to coerce a string to a number, returning `undefined` only when the
|
|
261
|
-
* result is `NaN`. Callers decide whether `undefined` means throw or fallback.
|
|
262
|
-
*
|
|
263
|
-
* @example
|
|
264
|
-
* ```ts
|
|
265
|
-
* tryCoerceNumber("42"); // 42
|
|
266
|
-
* tryCoerceNumber("abc"); // undefined
|
|
267
|
-
* tryCoerceNumber(""); // 0
|
|
268
|
-
* ```
|
|
269
|
-
*/
|
|
270
|
-
function tryCoerceNumber(raw) {
|
|
271
|
-
const num = Number(raw);
|
|
272
|
-
return Number.isNaN(num) ? void 0 : num;
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Coerces Crust boolean strings using the existing strict truthy spellings.
|
|
276
|
-
*
|
|
277
|
-
* @example
|
|
278
|
-
* ```ts
|
|
279
|
-
* coerceBooleanString("true"); // true
|
|
280
|
-
* coerceBooleanString("1"); // true
|
|
281
|
-
* coerceBooleanString("false"); // false
|
|
282
|
-
* ```
|
|
283
|
-
*/
|
|
284
|
-
function coerceBooleanString(raw) {
|
|
285
|
-
return raw === "true" || raw === "1";
|
|
286
|
-
}
|
|
287
|
-
//#endregion
|
|
288
|
-
//#region ../utils/src/schema.ts
|
|
289
|
-
/**
|
|
290
|
-
* Format an issue path into a dot-path string.
|
|
291
|
-
*
|
|
292
|
-
* - Numeric keys (array indexes) are rendered with bracket notation: `items[0]`
|
|
293
|
-
* - String/symbol keys are joined with dots: `flags.verbose`
|
|
294
|
-
* - An empty path array produces an empty string (root-level issue)
|
|
295
|
-
*
|
|
296
|
-
* @example
|
|
297
|
-
* ```ts
|
|
298
|
-
* formatPath(["flags", "verbose"]);
|
|
299
|
-
* // => "flags.verbose"
|
|
300
|
-
*
|
|
301
|
-
* formatPath(["args", 0]);
|
|
302
|
-
* // => "args[0]"
|
|
303
|
-
*
|
|
304
|
-
* formatPath([]);
|
|
305
|
-
* // => ""
|
|
306
|
-
* ```
|
|
307
|
-
*/
|
|
308
|
-
function isNumericPathSegment(segment) {
|
|
309
|
-
return typeof segment === "number";
|
|
310
|
-
}
|
|
311
|
-
function formatPath(path) {
|
|
312
|
-
return path.map((segment, index) => isNumericPathSegment(segment) ? `[${segment}]` : index > 0 ? `.${String(segment)}` : String(segment)).join("");
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Normalize a Standard Schema issue path to an array of `PropertyKey`.
|
|
316
|
-
*
|
|
317
|
-
* Standard Schema paths contain either bare `PropertyKey` values or
|
|
318
|
-
* `{ key: PropertyKey }` segment objects; both forms are flattened to
|
|
319
|
-
* plain `PropertyKey`. Returns an empty array for a root-level issue
|
|
320
|
-
* (`undefined`).
|
|
321
|
-
*/
|
|
322
|
-
function isStandardPathSegment(segment) {
|
|
323
|
-
return typeof segment === "object" && segment !== null && "key" in segment;
|
|
324
|
-
}
|
|
325
|
-
function normalizeStandardPath(path) {
|
|
326
|
-
if (!path) return [];
|
|
327
|
-
return path.map((segment) => isStandardPathSegment(segment) ? segment.key : segment);
|
|
328
|
-
}
|
|
256
|
+
//#region src/merge.ts
|
|
329
257
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* Applies an optional prefix (e.g. `["flags", "verbose"]`) to each issue
|
|
333
|
-
* path, then formats each path to its canonical dot-path string.
|
|
334
|
-
*
|
|
335
|
-
* @param issues — Raw Standard Schema issues from a failed validation
|
|
336
|
-
* @param prefix — Optional path segments prepended to each issue path
|
|
258
|
+
* Sets an own enumerable data property. Plain assignment would invoke the
|
|
259
|
+
* inherited `__proto__` setter instead of storing a `__proto__` field.
|
|
337
260
|
*/
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
path: formatPath(fullPath)
|
|
345
|
-
};
|
|
261
|
+
function setDocumentValue(document, key, value) {
|
|
262
|
+
Object.defineProperty(document, key, {
|
|
263
|
+
value,
|
|
264
|
+
writable: true,
|
|
265
|
+
enumerable: true,
|
|
266
|
+
configurable: true
|
|
346
267
|
});
|
|
347
268
|
}
|
|
348
|
-
//#endregion
|
|
349
|
-
//#region src/merge.ts
|
|
350
269
|
/**
|
|
351
270
|
* Applies field defaults to a persisted config object.
|
|
352
271
|
*
|
|
353
272
|
* For each field defined in `fields`:
|
|
354
|
-
* - If the key
|
|
273
|
+
* - If `persisted` has the key as an own property, the persisted value is used.
|
|
355
274
|
* - If the key is missing and the field has a `default`, the default is used.
|
|
356
275
|
* Defaults are deep-copied to prevent shared nested mutation.
|
|
357
276
|
* - If the key is missing and no default exists, the field is omitted
|
|
@@ -367,20 +286,14 @@ function normalizeStandardIssues(issues, prefix = []) {
|
|
|
367
286
|
*/
|
|
368
287
|
function applyFieldDefaults(persisted, fields, pruneUnknown = true) {
|
|
369
288
|
const result = {};
|
|
370
|
-
for (const [key, def] of Object.entries(fields)) if (persisted && key
|
|
371
|
-
else if ("default" in def && def.default !== void 0) result
|
|
289
|
+
for (const [key, def] of Object.entries(fields)) if (persisted && Object.hasOwn(persisted, key)) setDocumentValue(result, key, persisted[key]);
|
|
290
|
+
else if ("default" in def && def.default !== void 0) setDocumentValue(result, key, structuredClone(def.default));
|
|
372
291
|
if (!pruneUnknown && persisted !== void 0) {
|
|
373
|
-
for (const [key, value] of Object.entries(persisted)) if (!(key
|
|
292
|
+
for (const [key, value] of Object.entries(persisted)) if (!Object.hasOwn(fields, key)) setDocumentValue(result, key, value);
|
|
374
293
|
}
|
|
375
294
|
return result;
|
|
376
295
|
}
|
|
377
296
|
//#endregion
|
|
378
|
-
//#region ../utils/src/error.ts
|
|
379
|
-
/** Narrows a caught Node.js system error to its stable errno contract. */
|
|
380
|
-
function isErrnoException(error) {
|
|
381
|
-
return error instanceof Error && "code" in error && typeof error.code === "string";
|
|
382
|
-
}
|
|
383
|
-
//#endregion
|
|
384
297
|
//#region src/persistence.ts
|
|
385
298
|
/**
|
|
386
299
|
* Reads and parses a JSON config file from disk.
|
|
@@ -587,21 +500,25 @@ function createStore(options) {
|
|
|
587
500
|
function normalizeStateTypes(state) {
|
|
588
501
|
const normalized = { ...state };
|
|
589
502
|
for (const [key, def] of Object.entries(fields)) {
|
|
590
|
-
if (!(
|
|
503
|
+
if (!Object.hasOwn(normalized, key)) {
|
|
504
|
+
if (key in normalized) setDocumentValue(normalized, key, void 0);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (def.schema !== void 0) continue;
|
|
591
508
|
const value = normalized[key];
|
|
592
509
|
if (value === void 0) continue;
|
|
593
510
|
if (def.array === true && Array.isArray(value)) {
|
|
594
|
-
normalized
|
|
511
|
+
setDocumentValue(normalized, key, value.map((item) => coerceByType(item, def.type)));
|
|
595
512
|
continue;
|
|
596
513
|
}
|
|
597
|
-
normalized
|
|
514
|
+
setDocumentValue(normalized, key, coerceByType(value, def.type));
|
|
598
515
|
}
|
|
599
516
|
return normalized;
|
|
600
517
|
}
|
|
601
518
|
async function runFieldValidators(mutableState, operation) {
|
|
602
519
|
const issues = [];
|
|
603
520
|
for (const [key, def] of Object.entries(fields)) {
|
|
604
|
-
const value = mutableState[key];
|
|
521
|
+
const value = Object.hasOwn(mutableState, key) ? mutableState[key] : void 0;
|
|
605
522
|
if (value === void 0 && def.schema === void 0) {
|
|
606
523
|
if (def.default !== void 0) issues.push({
|
|
607
524
|
message: expectedTypeMessage(def),
|
|
@@ -633,7 +550,7 @@ function createStore(options) {
|
|
|
633
550
|
if (isFieldValueResult(result)) {
|
|
634
551
|
const transformed = result.value;
|
|
635
552
|
if (operation === "read") {
|
|
636
|
-
if (def.schema !== void 0) mutableState
|
|
553
|
+
if (def.schema !== void 0) setDocumentValue(mutableState, key, transformed);
|
|
637
554
|
continue;
|
|
638
555
|
}
|
|
639
556
|
if (def.schema === void 0 && !matchesDeclaredType(def, transformed)) {
|
|
@@ -662,7 +579,7 @@ function createStore(options) {
|
|
|
662
579
|
});
|
|
663
580
|
continue;
|
|
664
581
|
}
|
|
665
|
-
mutableState
|
|
582
|
+
setDocumentValue(mutableState, key, transformed);
|
|
666
583
|
}
|
|
667
584
|
}
|
|
668
585
|
}
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
2
|
+
"name": "@crustjs/store",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "DX-first, typed persistence for CLI apps with config/data/state/cache separation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "chenxin-yan",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/chenxin-yan/crust.git",
|
|
12
|
+
"directory": "packages/store"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://crustjs.com",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/chenxin-yan/crust/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cli",
|
|
20
|
+
"config",
|
|
21
|
+
"data",
|
|
22
|
+
"state",
|
|
23
|
+
"cache",
|
|
24
|
+
"store",
|
|
25
|
+
"persistence",
|
|
26
|
+
"xdg",
|
|
27
|
+
"bun",
|
|
28
|
+
"typescript"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@crustjs/utils": "^0.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@crustjs/config": "0.0.0",
|
|
47
|
+
"zod": "^4.5.4",
|
|
48
|
+
"vite-plus": "1.0.0-rc.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"typescript": "^7.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"typescript": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"bun": ">=1.4.0",
|
|
60
|
+
"node": ">=22",
|
|
61
|
+
"deno": ">=2.8"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "vp run build:task",
|
|
65
|
+
"dev": "vp pack --watch",
|
|
66
|
+
"check:types": "vp run check:types:task",
|
|
67
|
+
"test": "vp run test:task"
|
|
68
|
+
}
|
|
69
|
+
}
|