@crustjs/store 0.0.1 → 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/dist/index.js CHANGED
@@ -1,231 +1,4 @@
1
1
  // @bun
2
- // src/errors.ts
3
- class CrustStoreError extends Error {
4
- code;
5
- details;
6
- cause;
7
- constructor(code, message, details) {
8
- super(message);
9
- this.name = "CrustStoreError";
10
- this.code = code;
11
- this.details = details;
12
- }
13
- is(code) {
14
- return this.code === code;
15
- }
16
- withCause(cause) {
17
- this.cause = cause;
18
- return this;
19
- }
20
- }
21
- // src/path.ts
22
- import { homedir } from "os";
23
- import { join } from "path";
24
- var DEFAULT_STORE_NAME = "config";
25
- function getRuntimeEnv() {
26
- return {
27
- platform: process.platform,
28
- env: process.env,
29
- homedir: homedir()
30
- };
31
- }
32
- function validateAppName(appName) {
33
- if (!appName || appName.trim().length === 0) {
34
- throw new CrustStoreError("PATH", "appName must be a non-empty string", {
35
- path: appName ?? ""
36
- });
37
- }
38
- if (appName.includes("/") || appName.includes("\\")) {
39
- throw new CrustStoreError("PATH", "appName must not contain path separators", {
40
- path: appName
41
- });
42
- }
43
- }
44
- function validateName(name) {
45
- if (!name || name.trim().length === 0) {
46
- throw new CrustStoreError("PATH", "name must be a non-empty string", {
47
- path: name ?? ""
48
- });
49
- }
50
- if (name.includes("/") || name.includes("\\")) {
51
- throw new CrustStoreError("PATH", "name must not contain path separators", {
52
- path: name
53
- });
54
- }
55
- if (name.endsWith(".json")) {
56
- throw new CrustStoreError("PATH", "name must not include the .json extension", {
57
- path: name
58
- });
59
- }
60
- }
61
- function validateDirPath(dirPath) {
62
- if (!dirPath || dirPath.trim().length === 0) {
63
- throw new CrustStoreError("PATH", "dirPath must be a non-empty string", {
64
- path: dirPath ?? ""
65
- });
66
- }
67
- const isAbsolute = dirPath.startsWith("/") || /^[A-Za-z]:[/\\]/.test(dirPath);
68
- if (!isAbsolute) {
69
- throw new CrustStoreError("PATH", "dirPath must be an absolute path", {
70
- path: dirPath
71
- });
72
- }
73
- if (dirPath.endsWith(".json")) {
74
- throw new CrustStoreError("PATH", "dirPath must be a directory path, not a file path (should not end with .json)", {
75
- path: dirPath
76
- });
77
- }
78
- }
79
- function configDir(appName, env) {
80
- validateAppName(appName);
81
- const resolvedEnv = env ?? getRuntimeEnv();
82
- switch (resolvedEnv.platform) {
83
- case "linux": {
84
- const xdgConfig = resolvedEnv.env.XDG_CONFIG_HOME;
85
- const base = xdgConfig && xdgConfig.trim().length > 0 ? xdgConfig : join(resolvedEnv.homedir, ".config");
86
- return join(base, appName);
87
- }
88
- case "darwin": {
89
- return join(resolvedEnv.homedir, "Library", "Application Support", appName);
90
- }
91
- case "win32": {
92
- const appData = resolvedEnv.env.APPDATA;
93
- const base = appData && appData.trim().length > 0 ? appData : join(resolvedEnv.homedir, "AppData", "Roaming");
94
- return join(base, appName);
95
- }
96
- default:
97
- throw new CrustStoreError("PATH", `Unsupported platform: ${resolvedEnv.platform}`, {
98
- path: resolvedEnv.platform
99
- });
100
- }
101
- }
102
- function resolveStorePath(dirPath, name) {
103
- validateDirPath(dirPath);
104
- const storeName = name ?? DEFAULT_STORE_NAME;
105
- if (name !== undefined) {
106
- validateName(name);
107
- }
108
- return join(dirPath, `${storeName}.json`);
109
- }
110
- // src/merge.ts
111
- function applyFieldDefaults(persisted, fields) {
112
- const result = {};
113
- for (const [key, def] of Object.entries(fields)) {
114
- if (persisted && key in persisted) {
115
- result[key] = persisted[key];
116
- } else if ("default" in def && def.default !== undefined) {
117
- result[key] = Array.isArray(def.default) ? [...def.default] : def.default;
118
- }
119
- }
120
- return result;
121
- }
122
-
123
- // src/persistence.ts
124
- import { randomUUID } from "crypto";
125
- import {
126
- mkdir,
127
- readFile,
128
- rename,
129
- rm,
130
- unlink,
131
- writeFile
132
- } from "fs/promises";
133
- import { dirname, join as join2 } from "path";
134
- async function readJson(filePath) {
135
- let raw;
136
- try {
137
- raw = await readFile(filePath, "utf-8");
138
- } catch (err) {
139
- if (isEnoent(err)) {
140
- return;
141
- }
142
- throw new CrustStoreError("IO", `Failed to read config file: ${filePath}`, {
143
- path: filePath,
144
- operation: "read"
145
- }).withCause(err);
146
- }
147
- try {
148
- return JSON.parse(raw);
149
- } catch (err) {
150
- throw new CrustStoreError("PARSE", `Malformed JSON in config file: ${filePath}`, { path: filePath }).withCause(err);
151
- }
152
- }
153
- async function writeJson(filePath, data) {
154
- const dir = dirname(filePath);
155
- const tempPath = join2(dir, `.config-${randomUUID()}.tmp`);
156
- try {
157
- await mkdir(dir, { recursive: true });
158
- } catch (err) {
159
- throw new CrustStoreError("IO", `Failed to create config directory: ${dir}`, {
160
- path: filePath,
161
- operation: "write"
162
- }).withCause(err);
163
- }
164
- try {
165
- const json = JSON.stringify(data, null, "\t");
166
- await writeFile(tempPath, json, "utf-8");
167
- } catch (err) {
168
- await cleanupTempFile(tempPath);
169
- throw new CrustStoreError("IO", `Failed to write config file: ${filePath}`, {
170
- path: filePath,
171
- operation: "write"
172
- }).withCause(err);
173
- }
174
- try {
175
- await rename(tempPath, filePath);
176
- } catch (err) {
177
- await cleanupTempFile(tempPath);
178
- throw new CrustStoreError("IO", `Failed to finalize config file: ${filePath}`, {
179
- path: filePath,
180
- operation: "write"
181
- }).withCause(err);
182
- }
183
- }
184
- async function deleteJson(filePath) {
185
- try {
186
- await unlink(filePath);
187
- } catch (err) {
188
- if (isEnoent(err)) {
189
- return;
190
- }
191
- throw new CrustStoreError("IO", `Failed to delete config file: ${filePath}`, {
192
- path: filePath,
193
- operation: "delete"
194
- }).withCause(err);
195
- }
196
- }
197
- function isEnoent(err) {
198
- return err !== null && typeof err === "object" && "code" in err && err.code === "ENOENT";
199
- }
200
- async function cleanupTempFile(tempPath) {
201
- try {
202
- await rm(tempPath, { force: true });
203
- } catch {}
204
- }
205
-
206
- // src/store.ts
207
- function createStore(options) {
208
- const { dirPath, name, fields } = options;
209
- const filePath = resolveStorePath(dirPath, name);
210
- async function read() {
211
- const persisted = await readJson(filePath);
212
- return applyFieldDefaults(persisted, fields);
213
- }
214
- async function write(config) {
215
- await writeJson(filePath, config);
216
- }
217
- async function update(updater) {
218
- const current = await read();
219
- const updated = updater(current);
220
- await writeJson(filePath, updated);
221
- }
222
- async function reset() {
223
- await deleteJson(filePath);
224
- }
225
- return { read, write, update, reset };
226
- }
227
- export {
228
- createStore,
229
- configDir,
230
- CrustStoreError
231
- };
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.1",
4
- "description": "Minimal, type-safe config persistence for CLI apps",
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
  ],