progressive-zod 1.2.0 → 1.3.0

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.
@@ -27,8 +27,12 @@ Search for code patterns that handle data without runtime validation:
27
27
  For each boundary found, add a `progressive()` call. Use descriptive names that identify the boundary.
28
28
 
29
29
  ```typescript
30
+ // Server-side code (Node.js, Express, etc.)
30
31
  import { progressive } from "progressive-zod";
31
32
 
33
+ // Frontend code (Vite, Next.js client, etc.)
34
+ import { progressive } from "progressive-zod/client";
35
+
32
36
  // Before: untyped
33
37
  const data = req.body;
34
38
 
@@ -44,11 +48,15 @@ const data = UserCreate.parse(req.body);
44
48
 
45
49
  ### 3. Ensure progressive-zod is configured
46
50
 
47
- If there's no existing setup, add this to the app's entry point:
51
+ If there's no existing setup, add configuration to the app's entry point. Use the right import path based on the environment:
48
52
 
49
53
  ```typescript
54
+ // Server-side (Node.js, Express, etc.)
50
55
  import { configure } from "progressive-zod";
51
56
 
57
+ // Frontend (Vite, Next.js client, etc.) — avoids bundling ioredis
58
+ import { configure } from "progressive-zod/client";
59
+
52
60
  // For localhost development — no Redis needed
53
61
  configure({
54
62
  storage: "memory",
@@ -56,6 +64,8 @@ configure({
56
64
  });
57
65
  ```
58
66
 
67
+ **Important:** In frontend/browser code, always use `progressive-zod/client` to avoid bundler errors from server-only dependencies (ioredis). The client entry supports memory and Amplitude storage but not Redis.
68
+
59
69
  Add `.progressive-zod/` to `.gitignore` if not already there.
60
70
 
61
71
  ### 4. Show the user what you did
@@ -75,6 +85,7 @@ After instrumenting, summarize:
75
85
  - **progressive() never throws** — it always returns input unchanged, so it's safe to add anywhere
76
86
  - **Be conservative** — only instrument clear untyped boundaries, don't wrap already-typed internal code
77
87
  - **One progressive() per boundary** — don't double-wrap
88
+ - **Use the right import path** — use `progressive-zod/client` in frontend/browser code (Vite, Next.js client components, etc.) and `progressive-zod` in server-side code. The client entry excludes Redis to avoid bundler errors.
78
89
  - If the user already has Zod schemas for some boundaries, pass them as the second argument:
79
90
  ```typescript
80
91
  const UserCreate = progressive("UserCreate", existingUserSchema);
package/README.md CHANGED
@@ -28,6 +28,16 @@ app.post("/users", (req, res) => {
28
28
  });
29
29
  ```
30
30
 
31
+ ### Using in a frontend (Vite, Next.js, etc.)
32
+
33
+ If you're bundling with Vite or another frontend bundler, import from the client entry point to avoid pulling in server-only dependencies like `ioredis`:
34
+
35
+ ```typescript
36
+ import { progressive } from "progressive-zod/client";
37
+ ```
38
+
39
+ The client entry supports memory and Amplitude storage. If you need Redis, use the server entry (`progressive-zod`).
40
+
31
41
  Run your app, hit some endpoints, then:
32
42
 
33
43
  ```bash
@@ -113,7 +123,7 @@ This package includes a `/instrument` skill for [Claude Code](https://docs.anthr
113
123
 
114
124
  | Option | Env var | Default | Description |
115
125
  |--------|---------|---------|-------------|
116
- | `storage` | `PROGRESSIVE_ZOD_STORAGE` | `"memory"` | `"memory"` or `"redis"` |
126
+ | `storage` | `PROGRESSIVE_ZOD_STORAGE` | `"memory"` | `"memory"`, `"redis"`, or `"amplitude"` |
117
127
  | `redisUrl` | `PROGRESSIVE_ZOD_REDIS_URL` | `redis://localhost:6379` | Redis URL (only for redis storage) |
118
128
  | `keyPrefix` | `PROGRESSIVE_ZOD_KEY_PREFIX` | `pzod:` | Key namespace prefix |
119
129
  | `maxSamples` | `PROGRESSIVE_ZOD_MAX_SAMPLES` | `1000` | Max samples per type |
package/dist/cli/index.js CHANGED
@@ -15,6 +15,13 @@ var amplitude_exports = {};
15
15
  __export(amplitude_exports, {
16
16
  AmplitudeStorage: () => AmplitudeStorage
17
17
  });
18
+ function flattenToString(value) {
19
+ if (value === null || value === void 0) return String(value);
20
+ if (typeof value !== "object") return String(value);
21
+ if (Array.isArray(value)) return value.map(String).join(", ");
22
+ const entries = Object.entries(value);
23
+ return entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join("; ");
24
+ }
18
25
  var AmplitudeStorage;
19
26
  var init_amplitude = __esm({
20
27
  "src/storage/amplitude.ts"() {
@@ -32,17 +39,37 @@ var init_amplitude = __esm({
32
39
  }
33
40
  addViolation(_name, _violation) {
34
41
  }
35
- incrConform(name) {
42
+ incrConform(name, _sample) {
36
43
  this.client.track(
37
44
  "pzod:type_checked",
38
45
  { type_name: name, result: "conform" },
39
46
  { device_id: this.deviceId }
40
47
  );
41
48
  }
42
- incrViolate(name) {
49
+ incrViolate(name, sample, errors) {
50
+ const properties = {
51
+ type_name: name,
52
+ result: "violate"
53
+ };
54
+ if (sample) {
55
+ try {
56
+ const parsed = JSON.parse(sample);
57
+ properties.sample_type = typeof parsed;
58
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
59
+ properties.field_count = Object.keys(parsed).length;
60
+ }
61
+ properties.sample_preview = flattenToString(parsed).slice(0, 256);
62
+ } catch {
63
+ properties.sample_type = "unknown";
64
+ properties.sample_preview = sample.slice(0, 256);
65
+ }
66
+ }
67
+ if (errors) {
68
+ properties.validation_errors = errors.slice(0, 1024);
69
+ }
43
70
  this.client.track(
44
71
  "pzod:type_checked",
45
- { type_name: name, result: "violate" },
72
+ properties,
46
73
  { device_id: this.deviceId }
47
74
  );
48
75
  }
@@ -169,22 +196,14 @@ var init_redis = __esm({
169
196
  import { Command } from "commander";
170
197
 
171
198
  // src/storage/memory.ts
172
- import * as fs from "fs";
173
- import * as path from "path";
174
199
  var MemoryStorage = class {
175
200
  names = /* @__PURE__ */ new Set();
176
201
  data = /* @__PURE__ */ new Map();
177
202
  maxSamples;
178
203
  maxViolations;
179
- dataDir;
180
- flushTimer;
181
204
  constructor(config = {}) {
182
205
  this.maxSamples = config.maxSamples ?? 1e3;
183
206
  this.maxViolations = config.maxViolations ?? 1e3;
184
- this.dataDir = config.dataDir;
185
- if (this.dataDir) {
186
- this.loadFromDisk();
187
- }
188
207
  }
189
208
  getOrCreate(name) {
190
209
  let entry = this.data.get(name);
@@ -196,7 +215,6 @@ var MemoryStorage = class {
196
215
  }
197
216
  addName(name) {
198
217
  this.names.add(name);
199
- this.schedulePersist();
200
218
  }
201
219
  addSample(name, sample) {
202
220
  const entry = this.getOrCreate(name);
@@ -204,7 +222,6 @@ var MemoryStorage = class {
204
222
  if (entry.samples.length > this.maxSamples) {
205
223
  entry.samples.length = this.maxSamples;
206
224
  }
207
- this.schedulePersist();
208
225
  }
209
226
  addViolation(name, violation) {
210
227
  const entry = this.getOrCreate(name);
@@ -212,15 +229,12 @@ var MemoryStorage = class {
212
229
  if (entry.violations.length > this.maxViolations) {
213
230
  entry.violations.length = this.maxViolations;
214
231
  }
215
- this.schedulePersist();
216
232
  }
217
233
  incrConform(name) {
218
234
  this.getOrCreate(name).conform++;
219
- this.schedulePersist();
220
235
  }
221
236
  incrViolate(name) {
222
237
  this.getOrCreate(name).violate++;
223
- this.schedulePersist();
224
238
  }
225
239
  getNames() {
226
240
  return [...this.names].sort();
@@ -235,6 +249,79 @@ var MemoryStorage = class {
235
249
  const entry = this.getOrCreate(name);
236
250
  return { conform: entry.conform, violate: entry.violate };
237
251
  }
252
+ disconnect() {
253
+ }
254
+ };
255
+
256
+ // src/storage/resolve.ts
257
+ var currentConfig = {};
258
+ var currentStorage = null;
259
+ var storageFactory = async (config) => new MemoryStorage(config);
260
+ function _setStorageFactory(factory) {
261
+ storageFactory = factory;
262
+ }
263
+ function env(key) {
264
+ if (typeof process !== "undefined" && process.env) {
265
+ return process.env[key];
266
+ }
267
+ return void 0;
268
+ }
269
+ function getConfig() {
270
+ return {
271
+ storage: currentConfig.storage ?? env("PROGRESSIVE_ZOD_STORAGE") ?? "memory",
272
+ redisUrl: currentConfig.redisUrl ?? env("PROGRESSIVE_ZOD_REDIS_URL"),
273
+ keyPrefix: currentConfig.keyPrefix ?? env("PROGRESSIVE_ZOD_KEY_PREFIX") ?? "pzod:",
274
+ maxViolations: currentConfig.maxViolations ?? parseInt(env("PROGRESSIVE_ZOD_MAX_VIOLATIONS") ?? "1000", 10),
275
+ maxSamples: currentConfig.maxSamples ?? parseInt(env("PROGRESSIVE_ZOD_MAX_SAMPLES") ?? "1000", 10),
276
+ dataDir: currentConfig.dataDir ?? env("PROGRESSIVE_ZOD_DATA_DIR")
277
+ };
278
+ }
279
+ async function getStorage() {
280
+ if (currentStorage) return currentStorage;
281
+ const config = getConfig();
282
+ currentStorage = await storageFactory(config, currentConfig);
283
+ return currentStorage;
284
+ }
285
+ async function disconnectStorage() {
286
+ if (currentStorage) {
287
+ await currentStorage.disconnect();
288
+ currentStorage = null;
289
+ }
290
+ }
291
+
292
+ // src/storage/memory-server.ts
293
+ import * as fs from "fs";
294
+ import * as path from "path";
295
+ var PersistentMemoryStorage = class extends MemoryStorage {
296
+ dataDir;
297
+ flushTimer;
298
+ constructor(config = {}) {
299
+ super(config);
300
+ this.dataDir = config.dataDir;
301
+ if (this.dataDir) {
302
+ this.loadFromDisk();
303
+ }
304
+ }
305
+ addName(name) {
306
+ super.addName(name);
307
+ this.schedulePersist();
308
+ }
309
+ addSample(name, sample) {
310
+ super.addSample(name, sample);
311
+ this.schedulePersist();
312
+ }
313
+ addViolation(name, violation) {
314
+ super.addViolation(name, violation);
315
+ this.schedulePersist();
316
+ }
317
+ incrConform(name) {
318
+ super.incrConform(name);
319
+ this.schedulePersist();
320
+ }
321
+ incrViolate(name) {
322
+ super.incrViolate(name);
323
+ this.schedulePersist();
324
+ }
238
325
  disconnect() {
239
326
  if (this.flushTimer) {
240
327
  clearTimeout(this.flushTimer);
@@ -287,36 +374,6 @@ var MemoryStorage = class {
287
374
  }
288
375
  };
289
376
 
290
- // src/storage/resolve.ts
291
- var currentConfig = {};
292
- var currentStorage = null;
293
- var storageFactory = async (config) => new MemoryStorage(config);
294
- function _setStorageFactory(factory) {
295
- storageFactory = factory;
296
- }
297
- function getConfig() {
298
- return {
299
- storage: currentConfig.storage ?? process.env.PROGRESSIVE_ZOD_STORAGE ?? "memory",
300
- redisUrl: currentConfig.redisUrl ?? process.env.PROGRESSIVE_ZOD_REDIS_URL,
301
- keyPrefix: currentConfig.keyPrefix ?? process.env.PROGRESSIVE_ZOD_KEY_PREFIX ?? "pzod:",
302
- maxViolations: currentConfig.maxViolations ?? parseInt(process.env.PROGRESSIVE_ZOD_MAX_VIOLATIONS ?? "1000", 10),
303
- maxSamples: currentConfig.maxSamples ?? parseInt(process.env.PROGRESSIVE_ZOD_MAX_SAMPLES ?? "1000", 10),
304
- dataDir: currentConfig.dataDir ?? process.env.PROGRESSIVE_ZOD_DATA_DIR ?? ".progressive-zod"
305
- };
306
- }
307
- async function getStorage() {
308
- if (currentStorage) return currentStorage;
309
- const config = getConfig();
310
- currentStorage = await storageFactory(config, currentConfig);
311
- return currentStorage;
312
- }
313
- async function disconnectStorage() {
314
- if (currentStorage) {
315
- await currentStorage.disconnect();
316
- currentStorage = null;
317
- }
318
- }
319
-
320
377
  // src/storage/index.ts
321
378
  _setStorageFactory(async (config, userConfig) => {
322
379
  if (config.storage === "amplitude") {
@@ -331,7 +388,8 @@ _setStorageFactory(async (config, userConfig) => {
331
388
  const { RedisStorage: RedisStorage2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
332
389
  return new RedisStorage2(config);
333
390
  }
334
- return new MemoryStorage(config);
391
+ const serverConfig = { ...config, dataDir: config.dataDir ?? ".progressive-zod" };
392
+ return new PersistentMemoryStorage(serverConfig);
335
393
  });
336
394
 
337
395
  // src/cli/commands/list.ts
package/dist/client.cjs CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __esm = (fn, res) => function __init() {
9
7
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -20,14 +18,6 @@ var __copyProps = (to, from, except, desc) => {
20
18
  }
21
19
  return to;
22
20
  };
23
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
- // If the importer is in node compatibility mode or this is not an ESM
25
- // file that has been converted to a CommonJS file using a Babel-
26
- // compatible transform (i.e. "__esModule" has not been set), then set
27
- // "default" to the CommonJS "module.exports" for node compatibility.
28
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
- mod
30
- ));
31
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
22
 
33
23
  // src/storage/amplitude.ts
@@ -35,6 +25,13 @@ var amplitude_exports = {};
35
25
  __export(amplitude_exports, {
36
26
  AmplitudeStorage: () => AmplitudeStorage
37
27
  });
28
+ function flattenToString(value) {
29
+ if (value === null || value === void 0) return String(value);
30
+ if (typeof value !== "object") return String(value);
31
+ if (Array.isArray(value)) return value.map(String).join(", ");
32
+ const entries = Object.entries(value);
33
+ return entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join("; ");
34
+ }
38
35
  var AmplitudeStorage;
39
36
  var init_amplitude = __esm({
40
37
  "src/storage/amplitude.ts"() {
@@ -52,17 +49,37 @@ var init_amplitude = __esm({
52
49
  }
53
50
  addViolation(_name, _violation) {
54
51
  }
55
- incrConform(name) {
52
+ incrConform(name, _sample) {
56
53
  this.client.track(
57
54
  "pzod:type_checked",
58
55
  { type_name: name, result: "conform" },
59
56
  { device_id: this.deviceId }
60
57
  );
61
58
  }
62
- incrViolate(name) {
59
+ incrViolate(name, sample, errors) {
60
+ const properties = {
61
+ type_name: name,
62
+ result: "violate"
63
+ };
64
+ if (sample) {
65
+ try {
66
+ const parsed = JSON.parse(sample);
67
+ properties.sample_type = typeof parsed;
68
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
69
+ properties.field_count = Object.keys(parsed).length;
70
+ }
71
+ properties.sample_preview = flattenToString(parsed).slice(0, 256);
72
+ } catch {
73
+ properties.sample_type = "unknown";
74
+ properties.sample_preview = sample.slice(0, 256);
75
+ }
76
+ }
77
+ if (errors) {
78
+ properties.validation_errors = errors.slice(0, 1024);
79
+ }
63
80
  this.client.track(
64
81
  "pzod:type_checked",
65
- { type_name: name, result: "violate" },
82
+ properties,
66
83
  { device_id: this.deviceId }
67
84
  );
68
85
  }
@@ -99,22 +116,14 @@ __export(client_exports, {
99
116
  module.exports = __toCommonJS(client_exports);
100
117
 
101
118
  // src/storage/memory.ts
102
- var fs = __toESM(require("fs"), 1);
103
- var path = __toESM(require("path"), 1);
104
119
  var MemoryStorage = class {
105
120
  names = /* @__PURE__ */ new Set();
106
121
  data = /* @__PURE__ */ new Map();
107
122
  maxSamples;
108
123
  maxViolations;
109
- dataDir;
110
- flushTimer;
111
124
  constructor(config = {}) {
112
125
  this.maxSamples = config.maxSamples ?? 1e3;
113
126
  this.maxViolations = config.maxViolations ?? 1e3;
114
- this.dataDir = config.dataDir;
115
- if (this.dataDir) {
116
- this.loadFromDisk();
117
- }
118
127
  }
119
128
  getOrCreate(name) {
120
129
  let entry = this.data.get(name);
@@ -126,7 +135,6 @@ var MemoryStorage = class {
126
135
  }
127
136
  addName(name) {
128
137
  this.names.add(name);
129
- this.schedulePersist();
130
138
  }
131
139
  addSample(name, sample) {
132
140
  const entry = this.getOrCreate(name);
@@ -134,7 +142,6 @@ var MemoryStorage = class {
134
142
  if (entry.samples.length > this.maxSamples) {
135
143
  entry.samples.length = this.maxSamples;
136
144
  }
137
- this.schedulePersist();
138
145
  }
139
146
  addViolation(name, violation) {
140
147
  const entry = this.getOrCreate(name);
@@ -142,15 +149,12 @@ var MemoryStorage = class {
142
149
  if (entry.violations.length > this.maxViolations) {
143
150
  entry.violations.length = this.maxViolations;
144
151
  }
145
- this.schedulePersist();
146
152
  }
147
153
  incrConform(name) {
148
154
  this.getOrCreate(name).conform++;
149
- this.schedulePersist();
150
155
  }
151
156
  incrViolate(name) {
152
157
  this.getOrCreate(name).violate++;
153
- this.schedulePersist();
154
158
  }
155
159
  getNames() {
156
160
  return [...this.names].sort();
@@ -166,54 +170,6 @@ var MemoryStorage = class {
166
170
  return { conform: entry.conform, violate: entry.violate };
167
171
  }
168
172
  disconnect() {
169
- if (this.flushTimer) {
170
- clearTimeout(this.flushTimer);
171
- this.flushTimer = void 0;
172
- }
173
- if (this.dataDir) {
174
- this.persistToDisk();
175
- }
176
- }
177
- schedulePersist() {
178
- if (!this.dataDir || this.flushTimer) return;
179
- this.flushTimer = setTimeout(() => {
180
- this.flushTimer = void 0;
181
- this.persistToDisk();
182
- }, 500);
183
- }
184
- persistToDisk() {
185
- if (!this.dataDir) return;
186
- try {
187
- fs.mkdirSync(this.dataDir, { recursive: true });
188
- const snapshot = {
189
- names: [...this.names],
190
- types: Object.fromEntries(this.data)
191
- };
192
- fs.writeFileSync(
193
- path.join(this.dataDir, "data.json"),
194
- JSON.stringify(snapshot, null, 2)
195
- );
196
- } catch {
197
- }
198
- }
199
- loadFromDisk() {
200
- if (!this.dataDir) return;
201
- try {
202
- const raw = fs.readFileSync(
203
- path.join(this.dataDir, "data.json"),
204
- "utf-8"
205
- );
206
- const snapshot = JSON.parse(raw);
207
- if (Array.isArray(snapshot.names)) {
208
- for (const n of snapshot.names) this.names.add(n);
209
- }
210
- if (snapshot.types && typeof snapshot.types === "object") {
211
- for (const [key, val] of Object.entries(snapshot.types)) {
212
- this.data.set(key, val);
213
- }
214
- }
215
- } catch {
216
- }
217
173
  }
218
174
  };
219
175
 
@@ -231,14 +187,20 @@ function configure(config) {
231
187
  currentStorage = null;
232
188
  }
233
189
  }
190
+ function env(key) {
191
+ if (typeof process !== "undefined" && process.env) {
192
+ return process.env[key];
193
+ }
194
+ return void 0;
195
+ }
234
196
  function getConfig() {
235
197
  return {
236
- storage: currentConfig.storage ?? process.env.PROGRESSIVE_ZOD_STORAGE ?? "memory",
237
- redisUrl: currentConfig.redisUrl ?? process.env.PROGRESSIVE_ZOD_REDIS_URL,
238
- keyPrefix: currentConfig.keyPrefix ?? process.env.PROGRESSIVE_ZOD_KEY_PREFIX ?? "pzod:",
239
- maxViolations: currentConfig.maxViolations ?? parseInt(process.env.PROGRESSIVE_ZOD_MAX_VIOLATIONS ?? "1000", 10),
240
- maxSamples: currentConfig.maxSamples ?? parseInt(process.env.PROGRESSIVE_ZOD_MAX_SAMPLES ?? "1000", 10),
241
- dataDir: currentConfig.dataDir ?? process.env.PROGRESSIVE_ZOD_DATA_DIR ?? ".progressive-zod"
198
+ storage: currentConfig.storage ?? env("PROGRESSIVE_ZOD_STORAGE") ?? "memory",
199
+ redisUrl: currentConfig.redisUrl ?? env("PROGRESSIVE_ZOD_REDIS_URL"),
200
+ keyPrefix: currentConfig.keyPrefix ?? env("PROGRESSIVE_ZOD_KEY_PREFIX") ?? "pzod:",
201
+ maxViolations: currentConfig.maxViolations ?? parseInt(env("PROGRESSIVE_ZOD_MAX_VIOLATIONS") ?? "1000", 10),
202
+ maxSamples: currentConfig.maxSamples ?? parseInt(env("PROGRESSIVE_ZOD_MAX_SAMPLES") ?? "1000", 10),
203
+ dataDir: currentConfig.dataDir ?? env("PROGRESSIVE_ZOD_DATA_DIR")
242
204
  };
243
205
  }
244
206
  async function getStorage() {
@@ -360,13 +322,17 @@ var BatchProcessor = class {
360
322
  if (obs.schema) {
361
323
  const result = obs.schema.safeParse(JSON.parse(obs.serialized));
362
324
  if (result.success) {
363
- await storage.incrConform(obs.name);
325
+ await storage.incrConform(obs.name, obs.serialized);
364
326
  } else {
365
- await storage.incrViolate(obs.name);
327
+ const errors = result.error.issues.map((i) => {
328
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
329
+ return `${path}: ${i.message}`;
330
+ }).join("; ");
331
+ await storage.incrViolate(obs.name, obs.serialized, errors);
366
332
  await storage.addViolation(obs.name, obs.serialized);
367
333
  }
368
334
  } else {
369
- await storage.incrViolate(obs.name);
335
+ await storage.incrViolate(obs.name, obs.serialized, "no schema defined");
370
336
  await storage.addViolation(obs.name, obs.serialized);
371
337
  }
372
338
  }