@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +3 -81
  2. package/dist/index.js +27 -110
  3. package/package.json +68 -68
package/dist/index.d.ts CHANGED
@@ -1,84 +1,6 @@
1
- //#region ../utils/src/json.d.ts
2
- /** Any value representable in a JSON document. */
3
- type JsonValue = string | number | boolean | null | readonly JsonValue[] | JsonObject;
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 ../utils/src/json.ts
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
- * Normalize Standard Schema issues into canonical `ValidationIssue` objects.
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 normalizeStandardIssues(issues, prefix = []) {
339
- return issues.map((issue) => {
340
- const resolvedPath = normalizeStandardPath(issue.path);
341
- const fullPath = [...prefix, ...resolvedPath];
342
- return {
343
- message: issue.message,
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 exists in `persisted`, the persisted value is used.
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 in persisted) result[key] = persisted[key];
371
- else if ("default" in def && def.default !== void 0) result[key] = structuredClone(def.default);
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 in fields)) result[key] = value;
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 (!(key in normalized) || def.schema !== void 0) continue;
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[key] = value.map((item) => coerceByType(item, def.type));
511
+ setDocumentValue(normalized, key, value.map((item) => coerceByType(item, def.type)));
595
512
  continue;
596
513
  }
597
- normalized[key] = coerceByType(value, def.type);
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[key] = transformed;
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[key] = transformed;
582
+ setDocumentValue(mutableState, key, transformed);
666
583
  }
667
584
  }
668
585
  }
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
1
  {
2
- "name": "@crustjs/store",
3
- "version": "0.4.0",
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
- "scripts": {
43
- "build": "tsdown",
44
- "dev": "tsdown --watch",
45
- "check:types": "tsc --noEmit",
46
- "test": "bun test",
47
- "prepack": "cp ../../LICENSE LICENSE",
48
- "postpack": "rm -f LICENSE"
49
- },
50
- "devDependencies": {
51
- "@crustjs/config": "0.0.0",
52
- "zod": "^4.5.4",
53
- "@crustjs/utils": "0.0.0",
54
- "tsdown": "^0.23.0"
55
- },
56
- "peerDependencies": {
57
- "typescript": "^7.0.0"
58
- },
59
- "peerDependenciesMeta": {
60
- "typescript": {
61
- "optional": true
62
- }
63
- },
64
- "engines": {
65
- "bun": ">=1.4.0",
66
- "node": ">=22",
67
- "deno": ">=2.8"
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
+ }