Store
Typed JSON persistence for CLI config, data, state, and cache files
@crustjs/store creates typed JSON stores from field definitions. It is standalone: it does not inject values into a Crust Context.
Install
npm install @crustjs/storeQuick example
const store = createStore({
dirPath: configDir("my-cli"),
name: "config",
fields: {
theme: { type: "string", default: "light" },
verbose: { type: "boolean", default: false },
},
});
const initial = await store.read(); // { theme: "light", verbose: false }
await store.write({ theme: "dark", verbose: false });
await store.patch({ verbose: true });
await store.reset();
console.log(initial);Field types and defaults determine the state type. createStore() resolves <dirPath>/<name>.json once; dirPath must be absolute, and name cannot contain path separators or end in .json. Each name is an independent file, so use one store per file, such as config and auth.
Prop
Type
Operations
| Method | Behavior |
|---|---|
read() | Parse, coerce, merge missing defaults, and validate in memory. Never writes. |
write(state) | Validate a complete state, create parent directories, replace the file atomically, return it. |
update(fn) | Read and validate current state, call fn with it, then validate and persist the result. |
patch(partial) | Shallow-merge the supplied keys, then validate and persist. |
reset() | Remove the file; the next read() returns defaults. Idempotent. |
Prop
Type
update()rejects without callingfnwhen current state is invalid or a required value is missing. Usewrite()to initialize orpatch()to repair.- Mutations are atomic replacements, not locks. Await operations on the same file in sequence, and coordinate across processes yourself.
read()coerces core number and boolean strings; only"true"and"1"becometrue.- Unknown keys are dropped by default (
pruneUnknown: true); passfalseto keep them. Persisted falsy values are kept. - Malformed JSON and non-object roots reject with
PARSE. They never fall back to defaults.
Storage paths
| Helper | Linux and macOS | Windows | Override |
|---|---|---|---|
configDir | ~/.config/<app> | %APPDATA%\<app> | XDG_CONFIG_HOME; APPDATA on Windows |
dataDir | ~/.local/share/<app> | %LOCALAPPDATA%\<app>\Data | XDG_DATA_HOME; LOCALAPPDATA on Windows |
stateDir | ~/.local/state/<app> | %LOCALAPPDATA%\<app>\State | XDG_STATE_HOME; LOCALAPPDATA on Windows |
cacheDir | ~/.cache/<app> | %LOCALAPPDATA%\<app>\Cache | XDG_CACHE_HOME; LOCALAPPDATA on Windows |
Choose by intent: user configuration, durable app data, runtime state, or regenerable cache. macOS uses the XDG paths. Each helper accepts a PlatformEnv second argument for deterministic tests.
Secret files
const auth = createStore({
dirPath: configDir("my-cli"),
name: "auth",
fields: { token: { type: "string" } },
access: "private",
});On Unix, access: "private" writes the file with mode 0600 and a parent directory the store creates with 0700; an existing directory keeps its permissions. Pass { file, directory } for custom bits. Windows uses inherited ACLs, so the bits are not enforced there.
Validation
const ports = createStore({
dirPath: configDir("my-cli"),
name: "ports",
fields: {
port: {
type: "number",
default: 3000,
validate(value) {
if (value < 1 || value > 65535) throw new Error("Port must be 1-65535");
},
},
},
});
await ports.read();Prop
Type
- Core
validatecallbacks run on read and mutation and throw to reject. They may return{ value }to transform, but transforms apply only on mutation. - A schema field gives validation, transformation, defaults, and optionality to its Standard Schema v1
schema, and cannot also setdefaultorvalidate.read()returns the schema output without writing it. - Mutations revalidate transformed values and reject transforms whose result would read back differently.
- Mutations reject explicit
undefinedfor defaulted core fields, and values JSON cannot round-trip, such asNaN,Infinity,-0, sparse arrays,undefinedproperties, and cycles.
Errors
try {
await store.read();
} catch (error) {
if (error instanceof CrustStoreError && error.is("PARSE")) console.error(error.details.path);
}CrustStoreError.code is PATH, PARSE, IO, VALIDATION, or DEFINITION; .is(code) narrows details.
Store has no locking, encryption, migrations, other formats, or synchronous methods. The package entry point lists every export.