progressive-zod 1.2.0 → 1.2.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.
@@ -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
@@ -32,17 +32,37 @@ var init_amplitude = __esm({
32
32
  }
33
33
  addViolation(_name, _violation) {
34
34
  }
35
- incrConform(name) {
35
+ incrConform(name, _sample) {
36
36
  this.client.track(
37
37
  "pzod:type_checked",
38
38
  { type_name: name, result: "conform" },
39
39
  { device_id: this.deviceId }
40
40
  );
41
41
  }
42
- incrViolate(name) {
42
+ incrViolate(name, sample, errors) {
43
+ const properties = {
44
+ type_name: name,
45
+ result: "violate"
46
+ };
47
+ if (sample) {
48
+ try {
49
+ const parsed = JSON.parse(sample);
50
+ properties.sample_type = typeof parsed;
51
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
52
+ properties.field_count = Object.keys(parsed).length;
53
+ }
54
+ properties.sample_preview = sample.slice(0, 256);
55
+ } catch {
56
+ properties.sample_type = "unknown";
57
+ properties.sample_preview = sample.slice(0, 256);
58
+ }
59
+ }
60
+ if (errors) {
61
+ properties.validation_errors = errors.slice(0, 1024);
62
+ }
43
63
  this.client.track(
44
64
  "pzod:type_checked",
45
- { type_name: name, result: "violate" },
65
+ properties,
46
66
  { device_id: this.deviceId }
47
67
  );
48
68
  }
@@ -169,22 +189,14 @@ var init_redis = __esm({
169
189
  import { Command } from "commander";
170
190
 
171
191
  // src/storage/memory.ts
172
- import * as fs from "fs";
173
- import * as path from "path";
174
192
  var MemoryStorage = class {
175
193
  names = /* @__PURE__ */ new Set();
176
194
  data = /* @__PURE__ */ new Map();
177
195
  maxSamples;
178
196
  maxViolations;
179
- dataDir;
180
- flushTimer;
181
197
  constructor(config = {}) {
182
198
  this.maxSamples = config.maxSamples ?? 1e3;
183
199
  this.maxViolations = config.maxViolations ?? 1e3;
184
- this.dataDir = config.dataDir;
185
- if (this.dataDir) {
186
- this.loadFromDisk();
187
- }
188
200
  }
189
201
  getOrCreate(name) {
190
202
  let entry = this.data.get(name);
@@ -196,7 +208,6 @@ var MemoryStorage = class {
196
208
  }
197
209
  addName(name) {
198
210
  this.names.add(name);
199
- this.schedulePersist();
200
211
  }
201
212
  addSample(name, sample) {
202
213
  const entry = this.getOrCreate(name);
@@ -204,7 +215,6 @@ var MemoryStorage = class {
204
215
  if (entry.samples.length > this.maxSamples) {
205
216
  entry.samples.length = this.maxSamples;
206
217
  }
207
- this.schedulePersist();
208
218
  }
209
219
  addViolation(name, violation) {
210
220
  const entry = this.getOrCreate(name);
@@ -212,15 +222,12 @@ var MemoryStorage = class {
212
222
  if (entry.violations.length > this.maxViolations) {
213
223
  entry.violations.length = this.maxViolations;
214
224
  }
215
- this.schedulePersist();
216
225
  }
217
226
  incrConform(name) {
218
227
  this.getOrCreate(name).conform++;
219
- this.schedulePersist();
220
228
  }
221
229
  incrViolate(name) {
222
230
  this.getOrCreate(name).violate++;
223
- this.schedulePersist();
224
231
  }
225
232
  getNames() {
226
233
  return [...this.names].sort();
@@ -235,6 +242,79 @@ var MemoryStorage = class {
235
242
  const entry = this.getOrCreate(name);
236
243
  return { conform: entry.conform, violate: entry.violate };
237
244
  }
245
+ disconnect() {
246
+ }
247
+ };
248
+
249
+ // src/storage/resolve.ts
250
+ var currentConfig = {};
251
+ var currentStorage = null;
252
+ var storageFactory = async (config) => new MemoryStorage(config);
253
+ function _setStorageFactory(factory) {
254
+ storageFactory = factory;
255
+ }
256
+ function env(key) {
257
+ if (typeof process !== "undefined" && process.env) {
258
+ return process.env[key];
259
+ }
260
+ return void 0;
261
+ }
262
+ function getConfig() {
263
+ return {
264
+ storage: currentConfig.storage ?? env("PROGRESSIVE_ZOD_STORAGE") ?? "memory",
265
+ redisUrl: currentConfig.redisUrl ?? env("PROGRESSIVE_ZOD_REDIS_URL"),
266
+ keyPrefix: currentConfig.keyPrefix ?? env("PROGRESSIVE_ZOD_KEY_PREFIX") ?? "pzod:",
267
+ maxViolations: currentConfig.maxViolations ?? parseInt(env("PROGRESSIVE_ZOD_MAX_VIOLATIONS") ?? "1000", 10),
268
+ maxSamples: currentConfig.maxSamples ?? parseInt(env("PROGRESSIVE_ZOD_MAX_SAMPLES") ?? "1000", 10),
269
+ dataDir: currentConfig.dataDir ?? env("PROGRESSIVE_ZOD_DATA_DIR")
270
+ };
271
+ }
272
+ async function getStorage() {
273
+ if (currentStorage) return currentStorage;
274
+ const config = getConfig();
275
+ currentStorage = await storageFactory(config, currentConfig);
276
+ return currentStorage;
277
+ }
278
+ async function disconnectStorage() {
279
+ if (currentStorage) {
280
+ await currentStorage.disconnect();
281
+ currentStorage = null;
282
+ }
283
+ }
284
+
285
+ // src/storage/memory-server.ts
286
+ import * as fs from "fs";
287
+ import * as path from "path";
288
+ var PersistentMemoryStorage = class extends MemoryStorage {
289
+ dataDir;
290
+ flushTimer;
291
+ constructor(config = {}) {
292
+ super(config);
293
+ this.dataDir = config.dataDir;
294
+ if (this.dataDir) {
295
+ this.loadFromDisk();
296
+ }
297
+ }
298
+ addName(name) {
299
+ super.addName(name);
300
+ this.schedulePersist();
301
+ }
302
+ addSample(name, sample) {
303
+ super.addSample(name, sample);
304
+ this.schedulePersist();
305
+ }
306
+ addViolation(name, violation) {
307
+ super.addViolation(name, violation);
308
+ this.schedulePersist();
309
+ }
310
+ incrConform(name) {
311
+ super.incrConform(name);
312
+ this.schedulePersist();
313
+ }
314
+ incrViolate(name) {
315
+ super.incrViolate(name);
316
+ this.schedulePersist();
317
+ }
238
318
  disconnect() {
239
319
  if (this.flushTimer) {
240
320
  clearTimeout(this.flushTimer);
@@ -287,36 +367,6 @@ var MemoryStorage = class {
287
367
  }
288
368
  };
289
369
 
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
370
  // src/storage/index.ts
321
371
  _setStorageFactory(async (config, userConfig) => {
322
372
  if (config.storage === "amplitude") {
@@ -331,7 +381,8 @@ _setStorageFactory(async (config, userConfig) => {
331
381
  const { RedisStorage: RedisStorage2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
332
382
  return new RedisStorage2(config);
333
383
  }
334
- return new MemoryStorage(config);
384
+ const serverConfig = { ...config, dataDir: config.dataDir ?? ".progressive-zod" };
385
+ return new PersistentMemoryStorage(serverConfig);
335
386
  });
336
387
 
337
388
  // 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
@@ -52,17 +42,37 @@ var init_amplitude = __esm({
52
42
  }
53
43
  addViolation(_name, _violation) {
54
44
  }
55
- incrConform(name) {
45
+ incrConform(name, _sample) {
56
46
  this.client.track(
57
47
  "pzod:type_checked",
58
48
  { type_name: name, result: "conform" },
59
49
  { device_id: this.deviceId }
60
50
  );
61
51
  }
62
- incrViolate(name) {
52
+ incrViolate(name, sample, errors) {
53
+ const properties = {
54
+ type_name: name,
55
+ result: "violate"
56
+ };
57
+ if (sample) {
58
+ try {
59
+ const parsed = JSON.parse(sample);
60
+ properties.sample_type = typeof parsed;
61
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
62
+ properties.field_count = Object.keys(parsed).length;
63
+ }
64
+ properties.sample_preview = sample.slice(0, 256);
65
+ } catch {
66
+ properties.sample_type = "unknown";
67
+ properties.sample_preview = sample.slice(0, 256);
68
+ }
69
+ }
70
+ if (errors) {
71
+ properties.validation_errors = errors.slice(0, 1024);
72
+ }
63
73
  this.client.track(
64
74
  "pzod:type_checked",
65
- { type_name: name, result: "violate" },
75
+ properties,
66
76
  { device_id: this.deviceId }
67
77
  );
68
78
  }
@@ -99,22 +109,14 @@ __export(client_exports, {
99
109
  module.exports = __toCommonJS(client_exports);
100
110
 
101
111
  // src/storage/memory.ts
102
- var fs = __toESM(require("fs"), 1);
103
- var path = __toESM(require("path"), 1);
104
112
  var MemoryStorage = class {
105
113
  names = /* @__PURE__ */ new Set();
106
114
  data = /* @__PURE__ */ new Map();
107
115
  maxSamples;
108
116
  maxViolations;
109
- dataDir;
110
- flushTimer;
111
117
  constructor(config = {}) {
112
118
  this.maxSamples = config.maxSamples ?? 1e3;
113
119
  this.maxViolations = config.maxViolations ?? 1e3;
114
- this.dataDir = config.dataDir;
115
- if (this.dataDir) {
116
- this.loadFromDisk();
117
- }
118
120
  }
119
121
  getOrCreate(name) {
120
122
  let entry = this.data.get(name);
@@ -126,7 +128,6 @@ var MemoryStorage = class {
126
128
  }
127
129
  addName(name) {
128
130
  this.names.add(name);
129
- this.schedulePersist();
130
131
  }
131
132
  addSample(name, sample) {
132
133
  const entry = this.getOrCreate(name);
@@ -134,7 +135,6 @@ var MemoryStorage = class {
134
135
  if (entry.samples.length > this.maxSamples) {
135
136
  entry.samples.length = this.maxSamples;
136
137
  }
137
- this.schedulePersist();
138
138
  }
139
139
  addViolation(name, violation) {
140
140
  const entry = this.getOrCreate(name);
@@ -142,15 +142,12 @@ var MemoryStorage = class {
142
142
  if (entry.violations.length > this.maxViolations) {
143
143
  entry.violations.length = this.maxViolations;
144
144
  }
145
- this.schedulePersist();
146
145
  }
147
146
  incrConform(name) {
148
147
  this.getOrCreate(name).conform++;
149
- this.schedulePersist();
150
148
  }
151
149
  incrViolate(name) {
152
150
  this.getOrCreate(name).violate++;
153
- this.schedulePersist();
154
151
  }
155
152
  getNames() {
156
153
  return [...this.names].sort();
@@ -166,54 +163,6 @@ var MemoryStorage = class {
166
163
  return { conform: entry.conform, violate: entry.violate };
167
164
  }
168
165
  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
166
  }
218
167
  };
219
168
 
@@ -231,14 +180,20 @@ function configure(config) {
231
180
  currentStorage = null;
232
181
  }
233
182
  }
183
+ function env(key) {
184
+ if (typeof process !== "undefined" && process.env) {
185
+ return process.env[key];
186
+ }
187
+ return void 0;
188
+ }
234
189
  function getConfig() {
235
190
  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"
191
+ storage: currentConfig.storage ?? env("PROGRESSIVE_ZOD_STORAGE") ?? "memory",
192
+ redisUrl: currentConfig.redisUrl ?? env("PROGRESSIVE_ZOD_REDIS_URL"),
193
+ keyPrefix: currentConfig.keyPrefix ?? env("PROGRESSIVE_ZOD_KEY_PREFIX") ?? "pzod:",
194
+ maxViolations: currentConfig.maxViolations ?? parseInt(env("PROGRESSIVE_ZOD_MAX_VIOLATIONS") ?? "1000", 10),
195
+ maxSamples: currentConfig.maxSamples ?? parseInt(env("PROGRESSIVE_ZOD_MAX_SAMPLES") ?? "1000", 10),
196
+ dataDir: currentConfig.dataDir ?? env("PROGRESSIVE_ZOD_DATA_DIR")
242
197
  };
243
198
  }
244
199
  async function getStorage() {
@@ -360,13 +315,17 @@ var BatchProcessor = class {
360
315
  if (obs.schema) {
361
316
  const result = obs.schema.safeParse(JSON.parse(obs.serialized));
362
317
  if (result.success) {
363
- await storage.incrConform(obs.name);
318
+ await storage.incrConform(obs.name, obs.serialized);
364
319
  } else {
365
- await storage.incrViolate(obs.name);
320
+ const errors = result.error.issues.map((i) => {
321
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
322
+ return `${path}: ${i.message}`;
323
+ }).join("; ");
324
+ await storage.incrViolate(obs.name, obs.serialized, errors);
366
325
  await storage.addViolation(obs.name, obs.serialized);
367
326
  }
368
327
  } else {
369
- await storage.incrViolate(obs.name);
328
+ await storage.incrViolate(obs.name, obs.serialized, "no schema defined");
370
329
  await storage.addViolation(obs.name, obs.serialized);
371
330
  }
372
331
  }