@crustjs/store 0.1.0 → 0.1.1

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 CHANGED
@@ -85,7 +85,7 @@ const config = createStore({
85
85
  const data = createStore({
86
86
  dirPath: dataDir("my-cli"),
87
87
  fields: {
88
- bookmarks: { type: "array", default: [] },
88
+ bookmarks: { type: "string", array: true, default: [] },
89
89
  },
90
90
  });
91
91
 
@@ -320,8 +320,9 @@ Add per-field validation to enforce config integrity on every read, write, updat
320
320
  ### Schema-driven validation
321
321
 
322
322
  The easiest way to add validation is with the built-in `field()` factory.
323
- It builds a complete `FieldDef` from any Standard Schema — type, default,
324
- description, and the per-field validator are all derived in one call:
323
+ It builds a `FieldDef` from any [Standard Schema v1](https://standardschema.dev/)
324
+ object. Crust stores raw values; the schema validates and transforms them on
325
+ read/write.
325
326
 
326
327
  ```ts
327
328
  import { z } from "zod";
@@ -358,33 +359,34 @@ const store = createStore({
358
359
  });
359
360
  ```
360
361
 
362
+ Missing persisted values flow through the schema as `undefined`:
363
+
364
+ - `field(z.string().default("x"))` returns `"x"` on read when the field
365
+ is missing from disk.
366
+ - `field(z.string().optional())` returns `undefined`.
367
+ - `field(z.string())` fails read-time validation with a `VALIDATION`
368
+ error when the field is missing.
369
+
361
370
  #### Schema-derived defaults and TypeScript
362
371
 
363
372
  Standard Schema v1 has no spec-portable type-level access to schema
364
- defaults. As a result:
373
+ defaults. Crust does not inspect schemas to populate runtime metadata;
374
+ defaults are materialized on read by validating `undefined` through the
375
+ schema. As a result:
365
376
 
366
- - `field(z.string().default("x"))` populates `default: "x"` at runtime,
367
- but the inferred config type is `string | undefined` (NOT narrowed).
368
- - `field(z.string(), { default: "x" })` populates `default: "x"` AND
369
- narrows the inferred config type to `string`.
377
+ - `field(z.string().default("x"))` returns `"x"` on read, but the
378
+ inferred config type is `string | undefined` (NOT narrowed).
379
+ - `field(z.string(), { default: "x" })` keeps `"x"` as the Crust-level
380
+ default AND narrows the inferred config type to `string`.
370
381
 
371
382
  Prefer the explicit form when the field is required to always have a
372
383
  value at use sites.
373
384
 
374
385
  #### `field()` throws on invalid input
375
386
 
376
- `field()` throws `CrustStoreError("DEFINITION")` when handed a
377
- non-Standard-Schema value, or when the runtime CLI type cannot be
378
- inferred from the schema and `opts.type` was not supplied. The thrown
379
- error's `details.vendor` field carries the schema's vendor name (when
380
- available) to help locate the offending call site.
381
-
382
- > **Effect ≥ 3.14.2 required.** Effect 3.14.2 made `standardSchemaV1(...)`
383
- > wrappers expose `.ast`, which is what the validate registry walks. On
384
- > Effect 3.14.0 / 3.14.1 the wrapper is a plain object and introspection
385
- > silently fails. See
386
- > [`@crustjs/validate` Effect setup](https://crustjs.com/docs/modules/validate#quick-start--effect)
387
- > for the floor and a workaround.
387
+ `field()` throws `CrustStoreError("DEFINITION")` when handed a value that
388
+ is not a Standard Schema v1 object. `type` is optional for schema-backed
389
+ fields; pass it through `opts` only as legacy metadata for tooling.
388
390
 
389
391
  ### Custom validators
390
392
 
@@ -470,6 +472,7 @@ All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a
470
472
  | `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
471
473
  | `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
472
474
  | `VALIDATION` | Config fails validator on read, write, update, or patch | `{ operation, issues }` |
475
+ | `DEFINITION` | `field()` received an invalid (non-Standard-Schema) input | `{ vendor? }` |
473
476
 
474
477
  ### Catching errors by code
475
478
 
@@ -529,13 +532,13 @@ import type {
529
532
  StoreUpdater,
530
533
  FieldDef,
531
534
  FieldsDef,
535
+ FieldOptions,
532
536
  InferStoreConfig,
533
- StoreValidator,
534
- StoreValidatorResult,
535
537
  StoreValidatorIssue,
536
538
  StoreErrorCode,
537
- StoreValidationIssue,
538
539
  ValidationErrorDetails,
540
+ DefinitionErrorDetails,
541
+ ValueType,
539
542
  PlatformEnv,
540
543
  } from "@crustjs/store";
541
544
  ```
@@ -547,13 +550,13 @@ import type {
547
550
  | `StoreUpdater` | Updater function type `(current: T) => T`. |
548
551
  | `FieldDef` | Single field definition with `type`, optional `default`, and optional `validate`. |
549
552
  | `FieldsDef` | Record of field names to `FieldDef` definitions. |
553
+ | `FieldOptions` | Optional Crust metadata accepted by the `field()` factory (`type`, `default`, `array`, `description`). |
550
554
  | `InferStoreConfig` | Inferred store state type from a `FieldsDef` definition. |
551
- | `StoreValidator` | Validator function contract `(value: unknown) => StoreValidatorResult`. |
552
- | `StoreValidatorResult` | Discriminated union: `{ ok: true, value: T } \| { ok: false, issues: StoreValidatorIssue[] }`. |
553
555
  | `StoreValidatorIssue` | `{ message: string, path: string }`. |
554
- | `StoreValidationIssue` | Validation issue in error details payload. |
555
556
  | `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
556
- | `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION"`. |
557
+ | `DefinitionErrorDetails` | Error details for `DEFINITION` code: `{ vendor? }`. |
558
+ | `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION" \| "DEFINITION"`. |
559
+ | `ValueType` | Supported field type literals: `"string" \| "number" \| "boolean"`. |
557
560
  | `PlatformEnv` | Injectable platform environment for testing path helpers. |
558
561
  | `CrustStoreError` | Typed error class with `code`, `details`, and `cause`. |
559
562
 
package/dist/index.d.ts CHANGED
@@ -1,13 +1,6 @@
1
+ import { BaseValueType, ResolvePrimitive } from "@crustjs/utils";
1
2
  /** Supported type literals for store fields. */
2
- type ValueType = "string" | "number" | "boolean";
3
- /**
4
- * Resolves a type literal to its corresponding TypeScript primitive type.
5
- *
6
- * - `"string"` → `string`
7
- * - `"number"` → `number`
8
- * - `"boolean"` → `boolean`
9
- */
10
- type ResolvePrimitive<T extends ValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
3
+ type ValueType = BaseValueType;
11
4
  /** Shared fields present on every store field definition. */
12
5
  interface FieldDefBase<V> {
13
6
  /** Human-readable description for documentation and tooling. */
@@ -41,6 +34,8 @@ interface FieldDefBase<V> {
41
34
  } | Promise<{
42
35
  value: unknown;
43
36
  }>;
37
+ /** Internal marker: validate missing values as `undefined` for schema-backed fields. */
38
+ validateMissing?: true;
44
39
  }
45
40
  /** Base for scalar (non-array) fields — `array` must be omitted. */
46
41
  interface ScalarFieldBase<V> extends FieldDefBase<V> {
@@ -110,9 +105,18 @@ interface BooleanArrayFieldDef extends ArrayFieldBase<boolean[]> {
110
105
  * } satisfies FieldsDef;
111
106
  * ```
112
107
  */
113
- type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef;
108
+ interface RawScalarFieldDef extends ScalarFieldBase<unknown> {
109
+ type?: never;
110
+ default?: unknown;
111
+ }
112
+ interface RawArrayFieldDef extends ArrayFieldBase<unknown[]> {
113
+ type?: never;
114
+ default?: readonly unknown[];
115
+ }
116
+ type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef | RawScalarFieldDef | RawArrayFieldDef;
114
117
  /** Record mapping field names to their definitions. */
115
118
  type FieldsDef = Record<string, FieldDef>;
119
+ declare const FIELD_SCHEMA_OUTPUT: unique symbol;
116
120
  /**
117
121
  * Infer the resolved type for a single field definition.
118
122
  *
@@ -121,12 +125,20 @@ type FieldsDef = Record<string, FieldDef>;
121
125
  * - **no default** → `primitive | undefined` (optional)
122
126
  */
123
127
  type InferFieldValue<F extends FieldDef> = F extends {
128
+ readonly [FIELD_SCHEMA_OUTPUT]?: infer O;
129
+ } ? F extends {
130
+ default: infer D;
131
+ } ? D : O | undefined : F extends {
132
+ type: ValueType;
133
+ } ? F extends {
124
134
  array: true;
125
135
  } ? F extends {
126
136
  default: readonly ResolvePrimitive<F["type"]>[];
127
137
  } ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
128
138
  default: ResolvePrimitive<F["type"]>;
129
- } ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined;
139
+ } ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined : F extends {
140
+ default: infer D;
141
+ } ? D : unknown;
130
142
  /**
131
143
  * Maps a full {@link FieldsDef} record to the inferred config object type.
132
144
  *
@@ -339,11 +351,10 @@ interface ValidationErrorDetails {
339
351
  *
340
352
  * Returned when a `field()` call cannot produce a valid `FieldDef` from the
341
353
  * provided schema and options — e.g. the input is not a Standard Schema v1
342
- * object, or the schema is of an unknown vendor and `opts.type` was not
343
- * supplied so the runtime CLI type cannot be inferred.
354
+ * object.
344
355
  */
345
356
  interface DefinitionErrorDetails {
346
- /** The schema vendor name, when the failing input was a Standard Schema. */
357
+ /** The schema vendor name, when available. */
347
358
  vendor?: string;
348
359
  }
349
360
  /**
@@ -453,16 +464,14 @@ declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends
453
464
  */
454
465
  withCause(cause: unknown): this;
455
466
  }
456
- import { InferOutput, StandardSchema } from "@crustjs/schema-utils";
467
+ import { BaseValueType as BaseValueType2 } from "@crustjs/utils";
468
+ import { InferOutput, StandardSchema } from "@crustjs/utils/schema";
457
469
  /**
458
- * Optional overrides for the introspected `FieldDef` shape produced by
459
- * {@link field}.
470
+ * Optional Crust metadata for the `FieldDef` shape produced by {@link field}.
460
471
  *
461
- * Every key is optional. When automatic introspection covers a field
462
- * (`type`, `description`, `default`, `array`), explicit values override
463
- * the introspection silently. For schemas with unknown vendors (e.g.
464
- * Valibot, ArkType), `type` MUST be supplied explicitly because no
465
- * inference is available.
472
+ * Every key is optional. Crust does not infer metadata from schemas; schemas
473
+ * validate and transform actual runtime values, including missing values passed
474
+ * as `undefined`.
466
475
  *
467
476
  * No `validate` key — validation flows exclusively through the schema.
468
477
  * If extra checks are needed, refine the schema with `.refine(...)`
@@ -472,122 +481,79 @@ import { InferOutput, StandardSchema } from "@crustjs/schema-utils";
472
481
  * `default` key when the user wants a non-`undefined` field.
473
482
  */
474
483
  interface FieldOptions<T = unknown> {
475
- type?: "string" | "number" | "boolean";
484
+ type?: BaseValueType2;
476
485
  description?: string;
477
486
  /**
478
- * Default value for this field when the persisted state does not contain
479
- * a value for it. Passing `default` explicitly here narrows the inferred
480
- * config type from `T | undefined` to `T`. Schema-derived defaults
481
- * (e.g. `z.string().default("x")`) populate the runtime default but do
482
- * NOT narrow the TypeScript type — pass it explicitly here for tight
483
- * typing.
487
+ * Crust metadata default for this field when the persisted state does not
488
+ * contain a value for it. Schema defaults should usually live in the schema;
489
+ * missing persisted values are validated as `undefined`, so `.default()` works
490
+ * naturally at read time.
484
491
  */
485
492
  default?: T;
486
493
  /** Mark this field as an array (collects values into an array). */
487
494
  array?: true;
488
495
  }
489
- type ValueType2 = "string" | "number" | "boolean";
490
- type StripUndefined<T> = Exclude<T, undefined>;
491
- type PrimitiveToValueType<T> = [T] extends [string] ? "string" : [T] extends [number] ? "number" : [T] extends [boolean] ? "boolean" : ValueType2;
492
- /**
493
- * Resolve the runtime CLI value-type from a Standard Schema's output type.
494
- *
495
- * Array schemas are detected via `IsArrayOutput`; their element type drives
496
- * the value-type literal so `field(z.array(z.string()))` resolves to
497
- * `{ type: "string"; array: true }`.
498
- */
499
- type IsArrayOutput<S> = S extends StandardSchema<infer _I, infer Out> ? StripUndefined<Out> extends readonly unknown[] ? true : false : false;
500
- type ArrayElementOutput<S> = S extends StandardSchema<infer _I, infer Out> ? StripUndefined<Out> extends readonly (infer E)[] ? E : never : never;
501
- type ResolveScalarType<S> = S extends StandardSchema<infer _I, infer Out> ? PrimitiveToValueType<StripUndefined<Out>> : ValueType2;
502
- /** A scalar `FieldDef` with no narrowed default. */
503
- type ScalarFieldDef<T extends ValueType2> = {
504
- readonly type: T;
505
- readonly description?: string;
506
- readonly validate: (value: unknown) => Promise<{
507
- value: unknown;
508
- }>;
509
- };
510
- /** A scalar `FieldDef` with a narrowed default. */
511
- type ScalarFieldDefWithDefault<
512
- T extends ValueType2,
513
- D
514
- > = {
515
- readonly type: T;
496
+ type RawSchemaFieldDef<Out> = {
497
+ readonly type?: never;
498
+ readonly array?: never;
516
499
  readonly description?: string;
517
- readonly default: D;
518
500
  readonly validate: (value: unknown) => Promise<{
519
501
  value: unknown;
520
502
  }>;
503
+ readonly validateMissing: true;
504
+ readonly [FIELD_SCHEMA_OUTPUT]?: Out;
521
505
  };
522
- /** An array `FieldDef` with no narrowed default. */
523
- type ArrayFieldDef<T extends ValueType2> = {
524
- readonly type: T;
506
+ type SchemaFieldDefWithOptions<
507
+ Out,
508
+ Type extends FieldOptions["type"],
509
+ Array extends true | undefined,
510
+ D = never
511
+ > = Omit<RawSchemaFieldDef<Out>, "type" | "array"> & (Type extends NonNullable<FieldOptions["type"]> ? {
512
+ readonly type: Type;
513
+ } : {
514
+ readonly type?: never;
515
+ }) & (Array extends true ? {
525
516
  readonly array: true;
526
- readonly description?: string;
527
- readonly validate: (value: unknown) => Promise<{
528
- value: unknown;
529
- }>;
530
- };
531
- /** An array `FieldDef` with a narrowed default. */
532
- type ArrayFieldDefWithDefault<
533
- T extends ValueType2,
534
- D
535
- > = {
536
- readonly type: T;
537
- readonly array: true;
538
- readonly description?: string;
517
+ } : {
518
+ readonly array?: never;
519
+ }) & ([D] extends [never] ? unknown : {
539
520
  readonly default: D;
540
- readonly validate: (value: unknown) => Promise<{
541
- value: unknown;
542
- }>;
543
- };
521
+ });
544
522
  /**
545
523
  * `FieldDef` inferred from a Standard Schema, with no narrowed default.
546
524
  *
547
- * Schema-derived defaults are populated at runtime but do NOT narrow the
548
- * TypeScript type — Standard Schema v1 has no spec-portable type-level
549
- * access to defaults. Pass `field(schema, { default: x })` explicitly to
550
- * narrow.
551
- */
552
- type SchemaFieldDef<S extends StandardSchema> = IsArrayOutput<S> extends true ? ArrayFieldDef<PrimitiveToValueType<StripUndefined<ArrayElementOutput<S>>>> : ScalarFieldDef<ResolveScalarType<S>>;
553
- /**
554
- * `FieldDef` inferred from a Standard Schema, with the explicit `opts.default`
555
- * narrowed into the type.
525
+ * Standard Schema v1 has no spec-portable type-level access to defaults, so
526
+ * schema `.default()` does not narrow the inferred TypeScript type. Pass
527
+ * `field(schema, { default: x })` explicitly to narrow.
556
528
  */
557
- type SchemaFieldDefWithDefault<
558
- S extends StandardSchema,
559
- D
560
- > = IsArrayOutput<S> extends true ? ArrayFieldDefWithDefault<PrimitiveToValueType<StripUndefined<ArrayElementOutput<S>>>, D> : ScalarFieldDefWithDefault<ResolveScalarType<S>, D>;
529
+ type SchemaFieldDef<S extends StandardSchema> = RawSchemaFieldDef<InferOutput<S>>;
561
530
  /**
562
531
  * Define a `@crustjs/store` field from any Standard Schema v1.
563
532
  *
564
533
  * Returns a value that satisfies store's `FieldDef` discriminated union.
565
- * Auto-derives `type`, `array`, `description`, and `default` from the
566
- * schema (Zod and Effect natively; Valibot/ArkType via the
567
- * `validate(undefined)` fallback for defaults). Pass `opts` to override
568
- * any key explicitly — explicit values win silently.
534
+ * Crust stores raw values and lets the schema validate and transform them on
535
+ * read/write; missing persisted values flow through the schema as `undefined`,
536
+ * so `.optional()` and `.default()` behave naturally.
569
537
  *
570
538
  * The returned `validate` is an async function that throws an `Error` with
571
539
  * the schema's normalized issue messages on failure (matches store's
572
540
  * `FieldDef.validate` contract).
573
541
  *
574
- * **Type-level defaults**: schema-derived defaults populate at runtime but
575
- * do NOT narrow the inferred config type. For tight typing of
576
- * default-bearing fields, pass `default` via `opts`:
542
+ * **Type-level defaults**: Standard Schema v1 has no spec-portable type-level
543
+ * access to defaults. For tight typing of default-bearing fields, pass
544
+ * `default` via `opts`:
577
545
  *
578
546
  * ```ts
579
- * field(z.string().default("x")) // state: string | undefined
580
- * field(z.string(), { default: "x" }) // state: string
547
+ * field(z.string().default("x")) // value materializes on read; state: string | undefined
548
+ * field(z.string(), { default: "x" }) // Crust-level default; state: string
581
549
  * ```
582
550
  *
583
551
  * @param schema - Any Standard Schema v1 object (Zod schemas natively;
584
552
  * Effect schemas wrapped via `Schema.standardSchemaV1`;
585
553
  * Valibot/ArkType/Sury/etc. as-is)
586
- * @param opts - Optional store-field metadata; explicit keys override
587
- * the introspected values silently
554
+ * @param opts - Optional Crust-level metadata
588
555
  * @throws {CrustStoreError} With code `"DEFINITION"` when the input is not
589
- * a Standard Schema, or when the runtime CLI type cannot be inferred
590
- * and `opts.type` was not supplied.
556
+ * a Standard Schema v1 object.
591
557
  *
592
558
  * @example Zod
593
559
  * ```ts
@@ -620,11 +586,22 @@ type SchemaFieldDefWithDefault<
620
586
  declare function field<S extends StandardSchema>(schema: S): SchemaFieldDef<S>;
621
587
  declare function field<
622
588
  S extends StandardSchema,
623
- D extends InferOutput<S>
589
+ D extends InferOutput<S>,
590
+ const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
591
+ const Array extends true | undefined = undefined
624
592
  >(schema: S, opts: FieldOptions<InferOutput<S>> & {
625
593
  default: D;
626
- }): SchemaFieldDefWithDefault<S, D>;
627
- declare function field<S extends StandardSchema>(schema: S, opts: FieldOptions<InferOutput<S>>): SchemaFieldDef<S>;
594
+ type?: Type;
595
+ array?: Array;
596
+ }): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array, D>;
597
+ declare function field<
598
+ S extends StandardSchema,
599
+ const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
600
+ const Array extends true | undefined = undefined
601
+ >(schema: S, opts: FieldOptions<InferOutput<S>> & {
602
+ type?: Type;
603
+ array?: Array;
604
+ }): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array>;
628
605
  /**
629
606
  * Encapsulates runtime environment values needed for path resolution.
630
607
  *
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- class W extends Error{code;details;cause;constructor(q,H,G){super(H);this.name="CrustStoreError",this.code=q,this.details=G}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{extractDefault as m,inferOptions as d,isStandardSchema as p,normalizeStandardIssues as o}from"@crustjs/schema-utils";function n(q){return async(H)=>{let G=await q["~standard"].validate(H);if(G.issues){let L=o(G.issues).map((Q)=>Q.path?`${Q.path}: ${Q.message}`:Q.message);throw Error(L.join("; "))}return{value:G.value}}}function i(q,H){if(!p(q))throw new W("DEFINITION",`field(): argument must be a Standard Schema v1 object (got ${typeof q})`,{});let G=q["~standard"]?.vendor??"unknown",K=`field (vendor: "${G}")`,L=d(q,"field",K),Q=H?.type??L.type;if(!Q)throw new W("DEFINITION",`${K}: unable to infer field type from schema. Pass an explicit { type: "string" | "number" | "boolean" } in options. If this is an Effect schema, wrap it with Schema.standardSchemaV1(...) before passing it here.`,{vendor:G});let J=H?.array===!0||L.multiple===!0,V=H?.description??L.description,x=H&&"default"in H?{ok:!0,value:H.default}:m(q),T=n(q),O={type:Q,validate:T};if(J)O.array=!0;if(V!==void 0)O.description=V;if(x.ok)O.default=x.value;return O}import{homedir as l}from"os";import{join as B}from"path";var s="config";function U(){return{platform:process.platform,env:process.env,homedir:l()}}function j(q){if(!q||q.trim().length===0)throw new W("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new W("PATH","appName must not contain path separators",{path:q})}function t(q){if(!q||q.trim().length===0)throw new W("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new W("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new W("PATH","name must not include the .json extension",{path:q})}function r(q){if(!q||q.trim().length===0)throw new W("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new W("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new W("PATH","dirPath must be a directory path, not a file path (should not end with .json)",{path:q})}function C(q,H,G,K){let L=q.env[H],Q=L&&L.trim().length>0?L:B(q.homedir,...G);return B(Q,K)}function D(q){throw new W("PATH",`Unsupported platform: ${q}`,{path:q})}function a(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let K=G.env.APPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Roaming");return B(L,q)}default:D(G.platform)}}function e(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let K=G.env.LOCALAPPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Local");return B(L,q,"Data")}default:D(G.platform)}}function qq(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let K=G.env.LOCALAPPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Local");return B(L,q,"State")}default:D(G.platform)}}function Gq(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let K=G.env.LOCALAPPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Local");return B(L,q,"Cache")}default:D(G.platform)}}function S(q,H){r(q);let G=H??s;if(H!==void 0)t(H);return B(q,`${G}.json`)}function w(q,H,G=!0){let K={};for(let[L,Q]of Object.entries(H))if(q&&L in q){let J=q[L];K[L]=Array.isArray(J)?[...J]:J}else if("default"in Q&&Q.default!==void 0)K[L]=Array.isArray(Q.default)?[...Q.default]:Q.default;if(!G&&q!==void 0){for(let[L,Q]of Object.entries(q))if(!(L in H))K[L]=Array.isArray(Q)?[...Q]:Q}return K}import{randomUUID as Hq}from"crypto";import{mkdir as Kq,readFile as Lq,rename as Qq,rm as Wq,unlink as Xq,writeFile as Yq}from"fs/promises";import{dirname as Zq,join as $q}from"path";async function y(q){let H;try{H=await Lq(q,"utf-8")}catch(G){if(E(G))return;throw new W("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(G)}try{return JSON.parse(H)}catch(G){throw new W("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(G)}}async function z(q,H){let G=Zq(q),K=$q(G,`.config-${Hq()}.tmp`);try{await Kq(G,{recursive:!0})}catch(L){throw new W("IO",`Failed to create config directory: ${G}`,{path:q,operation:"write"}).withCause(L)}try{let L=JSON.stringify(H,null,"\t");await Yq(K,L,"utf-8")}catch(L){throw await g(K),new W("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(L)}try{await Qq(K,q)}catch(L){throw await g(K),new W("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(L)}}async function k(q){try{await Xq(q)}catch(H){if(E(H))return;throw new W("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(H)}}function E(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function g(q){try{await Wq(q,{force:!0})}catch{}}function N(q){return typeof q==="object"&&q!==null&&"value"in q&&!("ok"in q)}function Mq(q){let{dirPath:H,name:G,fields:K,pruneUnknown:L}=q,Q=S(H,G),J=L??!0;function V(X,Z){if(Z==="number"&&typeof X==="string"){let Y=Number(X);return Number.isNaN(Y)?X:Y}if(Z==="boolean"&&typeof X==="string")return X==="true"||X==="1";return X}function x(X){let Y={...X};for(let[$,M]of Object.entries(K)){if(!($ in Y))continue;let I=Y[$];if(M.array===!0&&Array.isArray(I)){Y[$]=I.map((_)=>V(_,M.type));continue}Y[$]=V(I,M.type)}return Y}async function T(X,Z){let Y=[],$=X;for(let[M,I]of Object.entries(K)){if(!I.validate)continue;let _=$[M];if(_===void 0)continue;let R;try{R=await I.validate(_)}catch(F){let b=F instanceof Error?F.message:"Validation failed";Y.push({message:b,path:M});continue}if(R===void 0)continue;if(N(R)){let F=R.value;if(Z==="read")continue;if(!Bun.deepEquals(F,_)){let b;try{b=await I.validate(F)}catch(A){let u=A instanceof Error?A.message:"re-validation failed";Y.push({message:`read-unstable transform: ${u}`,path:M});continue}if(b!==void 0&&(!N(b)||!Bun.deepEquals(b.value,F))){Y.push({message:"read-unstable transform: output would be transformed again on re-read",path:M});continue}$[M]=F}continue}throw TypeError("FieldDef.validate must return void or { value } (see @crustjs/store validation docs).")}if(Y.length>0){let M=Y.map((_)=>` - ${_.path}: ${_.message}`),I=`Store validation failed (${Z})
3
- ${M.join(`
4
- `)}`;throw new W("VALIDATION",I,{operation:Z,issues:Y})}}async function O(){let X=await y(Q),Z=w(X,K,J);return x(Z)}async function P(){let X=await O();return await T(X,"read"),X}async function c(X){let Z=x(X);await T(Z,"write"),await z(Q,Z)}async function f(X){let Z=await O(),Y=X(Z),$=x(Y);await T($,"update"),await z(Q,$)}async function h(X){let Y={...await O(),...X},$=x(Y);await T($,"patch"),await z(Q,$)}async function v(){await k(Q)}return{read:P,write:c,update:f,patch:h,reset:v}}export{qq as stateDir,i as field,e as dataDir,Mq as createStore,a as configDir,Gq as cacheDir,W as CrustStoreError};
2
+ class X extends Error{code;details;cause;constructor(q,H,G){super(H);this.name="CrustStoreError",this.code=q,this.details=G}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{isStandardSchema as m,normalizeStandardIssues as p}from"@crustjs/utils/schema";function d(q){return async(H)=>{let G=await q["~standard"].validate(H);if(G.issues){let Q=p(G.issues).map((Y)=>Y.path?`${Y.path}: ${Y.message}`:Y.message);throw Error(Q.join("; "))}return{value:G.value}}}function n(q,H){if(!m(q))throw new X("DEFINITION",`field(): argument must be a Standard Schema v1 object (got ${typeof q})`,{});let K={validate:d(q),validateMissing:!0};if(H?.type!==void 0)K.type=H.type;if(H?.array===!0)K.array=!0;if(H?.description!==void 0)K.description=H.description;if(H&&"default"in H)K.default=H.default;return K}import{homedir as o}from"os";import{join as Z}from"path";var l="config";function R(){return{platform:process.platform,env:process.env,homedir:o()}}function U(q){if(!q||q.trim().length===0)throw new X("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new X("PATH","appName must not contain path separators",{path:q})}function i(q){if(!q||q.trim().length===0)throw new X("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new X("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new X("PATH","name must not include the .json extension",{path:q})}function s(q){if(!q||q.trim().length===0)throw new X("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new X("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new X("PATH","dirPath must be a directory path, not a file path (should not end with .json)",{path:q})}function b(q,H,G,K){let Q=q.env[H],Y=Q&&Q.trim().length>0?Q:Z(q.homedir,...G);return Z(Y,K)}function A(q){throw new X("PATH",`Unsupported platform: ${q}`,{path:q})}function t(q,H){U(q);let G=H??R();switch(G.platform){case"linux":case"darwin":return b(G,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let K=G.env.APPDATA,Q=K&&K.trim().length>0?K:Z(G.homedir,"AppData","Roaming");return Z(Q,q)}default:A(G.platform)}}function r(q,H){U(q);let G=H??R();switch(G.platform){case"linux":case"darwin":return b(G,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:Z(G.homedir,"AppData","Local");return Z(Q,q,"Data")}default:A(G.platform)}}function a(q,H){U(q);let G=H??R();switch(G.platform){case"linux":case"darwin":return b(G,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:Z(G.homedir,"AppData","Local");return Z(Q,q,"State")}default:A(G.platform)}}function e(q,H){U(q);let G=H??R();switch(G.platform){case"linux":case"darwin":return b(G,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:Z(G.homedir,"AppData","Local");return Z(Q,q,"Cache")}default:A(G.platform)}}function k(q,H){s(q);let G=H??l;if(H!==void 0)i(H);return Z(q,`${G}.json`)}import{coerceBooleanString as Mq,tryCoerceNumber as Wq}from"@crustjs/utils";function w(q,H,G=!0){let K={};for(let[Q,Y]of Object.entries(H))if(q&&Q in q){let x=q[Q];K[Q]=Array.isArray(x)?[...x]:x}else if("default"in Y&&Y.default!==void 0)K[Q]=Array.isArray(Y.default)?[...Y.default]:Y.default;if(!G&&q!==void 0){for(let[Q,Y]of Object.entries(q))if(!(Q in H))K[Q]=Array.isArray(Y)?[...Y]:Y}return K}import{randomUUID as qq}from"crypto";import{mkdir as Gq,readFile as Hq,rename as Kq,rm as Qq,unlink as Xq,writeFile as Yq}from"fs/promises";import{dirname as $q,join as Lq}from"path";async function S(q){let H;try{H=await Hq(q,"utf-8")}catch(G){if(N(G))return;throw new X("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(G)}try{return JSON.parse(H)}catch(G){throw new X("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(G)}}async function C(q,H){let G=$q(q),K=Lq(G,`.config-${qq()}.tmp`);try{await Gq(G,{recursive:!0})}catch(Q){throw new X("IO",`Failed to create config directory: ${G}`,{path:q,operation:"write"}).withCause(Q)}try{let Q=JSON.stringify(H,null,"\t");await Yq(K,Q,"utf-8")}catch(Q){throw await y(K),new X("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(Q)}try{await Kq(K,q)}catch(Q){throw await y(K),new X("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(Q)}}async function g(q){try{await Xq(q)}catch(H){if(N(H))return;throw new X("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(H)}}function N(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function y(q){try{await Qq(q,{force:!0})}catch{}}function E(q){return typeof q==="object"&&q!==null&&"value"in q&&!("ok"in q)}function Oq(q){let{dirPath:H,name:G,fields:K,pruneUnknown:Q}=q,Y=k(H,G),x=Q??!0;function j($,L){if(L===void 0)return $;if(L==="number"&&typeof $==="string")return Wq($)??$;if(L==="boolean"&&typeof $==="string")return Mq($);return $}function T($){let M={...$};for(let[W,O]of Object.entries(K)){if(!(W in M))continue;let _=M[W];if(O.array===!0&&Array.isArray(_)){M[W]=_.map((B)=>j(B,O.type));continue}M[W]=j(_,O.type)}return M}async function V($,L){let M=[],W=$;for(let[O,_]of Object.entries(K)){if(!_.validate)continue;let B=W[O];if(B===void 0&&_.validateMissing!==!0)continue;let F;try{F=await _.validate(B)}catch(I){let J=I instanceof Error?I.message:"Validation failed";M.push({message:J,path:O});continue}if(F===void 0)continue;if(E(F)){let I=F.value;if(L==="read"){if(B===void 0)W[O]=I;continue}if(!Bun.deepEquals(I,B)){let J;try{J=await _.validate(I)}catch(z){let u=z instanceof Error?z.message:"re-validation failed";M.push({message:`read-unstable transform: ${u}`,path:O});continue}if(J!==void 0&&(!E(J)||!Bun.deepEquals(J.value,I))){M.push({message:"read-unstable transform: output would be transformed again on re-read",path:O});continue}W[O]=I}continue}throw TypeError("FieldDef.validate must return void or { value } (see @crustjs/store validation docs).")}if(M.length>0){let O=M.map((B)=>` - ${B.path}: ${B.message}`),_=`Store validation failed (${L})
3
+ ${O.join(`
4
+ `)}`;throw new X("VALIDATION",_,{operation:L,issues:M})}}async function D(){let $=await S(Y),L=w($,K,x);return T(L)}async function P(){let $=await D();return await V($,"read"),$}async function c($){let L=T($);await V(L,"write"),await C(Y,L)}async function f($){let L=await D(),M=$(L),W=T(M);await V(W,"update"),await C(Y,W)}async function v($){let M={...await D(),...$},W=T(M);await V(W,"patch"),await C(Y,W)}async function h(){await g(Y)}return{read:P,write:c,update:f,patch:v,reset:h}}export{a as stateDir,n as field,r as dataDir,Oq as createStore,t as configDir,e as cacheDir,X as CrustStoreError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/store",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DX-first, typed persistence for CLI apps with config/data/state/cache separation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,13 +48,13 @@
48
48
  "devDependencies": {
49
49
  "@crustjs/config": "0.0.0",
50
50
  "bunup": "^0.16.31",
51
- "effect": "3.14.2",
51
+ "effect": "3.21.2",
52
52
  "zod": "^4.0.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "typescript": "^6.0.3"
56
56
  },
57
57
  "dependencies": {
58
- "@crustjs/schema-utils": "0.0.1"
58
+ "@crustjs/utils": "0.0.2"
59
59
  }
60
60
  }