zod-compiler 2.0.5 → 2.0.6

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.
Files changed (34) hide show
  1. package/README.md +97 -118
  2. package/dist/core/codegen/build-path.js +1 -1
  3. package/dist/core/codegen/context.d.ts +6 -5
  4. package/dist/core/codegen/context.d.ts.map +1 -1
  5. package/dist/core/codegen/context.js +10 -8
  6. package/dist/core/codegen/context.js.map +1 -1
  7. package/dist/core/codegen/index.js +1 -1
  8. package/dist/core/codegen/issue-decls.d.ts +18 -1
  9. package/dist/core/codegen/issue-decls.d.ts.map +1 -1
  10. package/dist/core/codegen/issue-decls.js +19 -1
  11. package/dist/core/codegen/issue-decls.js.map +1 -1
  12. package/dist/core/codegen/schemas/array.js +2 -2
  13. package/dist/core/codegen/schemas/array.js.map +1 -1
  14. package/dist/core/codegen/schemas/custom.js +1 -1
  15. package/dist/core/codegen/schemas/discriminated-union.js +1 -1
  16. package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
  17. package/dist/core/codegen/schemas/effect.js +2 -2
  18. package/dist/core/codegen/schemas/effect.js.map +1 -1
  19. package/dist/core/codegen/schemas/fallback.js +1 -1
  20. package/dist/core/codegen/schemas/map.js +1 -1
  21. package/dist/core/codegen/schemas/number.js +1 -1
  22. package/dist/core/codegen/schemas/object.js +3 -3
  23. package/dist/core/codegen/schemas/object.js.map +1 -1
  24. package/dist/core/codegen/schemas/record.js +2 -2
  25. package/dist/core/codegen/schemas/record.js.map +1 -1
  26. package/dist/core/codegen/schemas/sizeable.js +1 -1
  27. package/dist/core/codegen/schemas/string.js +1 -1
  28. package/dist/core/codegen/schemas/tuple.js +2 -2
  29. package/dist/core/codegen/schemas/tuple.js.map +1 -1
  30. package/dist/core/codegen/schemas/union.js +1 -1
  31. package/dist/runtime.d.ts +1 -0
  32. package/dist/runtime.js +1 -0
  33. package/dist/unplugin/virtual.js +1 -1
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -2,14 +2,12 @@
2
2
 
3
3
  **Compile Zod schemas into zero-overhead validation functions at build time.**
4
4
 
5
- Keep your existing Zod schemas. Get **up to 44x faster** validation, and up to **46x** on rejected
5
+ Keep your existing Zod schemas. Get **up to 43x faster** validation, and up to **48x** on rejected
6
6
  input. No code changes required.
7
7
 
8
- Requires **Zod ≥ 4.5**. Compiled output reproduces 4.5's semantics exactly, down to code-point string
9
- lengths, symbol-keyed shapes and tuple issue order, so it does not match earlier 4.x releases. Stay on
10
- zod-compiler 1.x for Zod 4.0–4.4. An older Zod is refused with an explicit error (the build plugin
11
- and `jit()` warn once and leave the schemas as plain Zod) rather than compiled into validators that
12
- disagree with it.
8
+ Requires **Zod ≥ 4.5**; compiled output reproduces 4.5's semantics exactly, so it does not match
9
+ earlier 4.x releases. Use zod-compiler 1.x for Zod 4.0–4.4. An older Zod is refused with an explicit
10
+ error; the build plugin and `jit()` warn once and leave schemas as plain Zod.
13
11
 
14
12
  - [What Gets Compiled](#what-gets-compiled)
15
13
  - [Schema Hoisting](#schema-hoisting)
@@ -28,16 +26,12 @@ loads pre-generated validators.
28
26
  | | zod-compiler (build plugins / CLI) | Zod `z.compile()` |
29
27
  | -------------------------------------- | -------------------------------------------- | --------------------------------------------- |
30
28
  | Compilation | Build time (true AOT) | Runtime (`z.compile()` or the first parse) |
31
- | Reported validation speedup | Up to 44x; up to 46x on rejected input | ~9x in Zod's headline example |
32
- | Uses `new Function()` at runtime\* | No | Yes |
29
+ | Reported validation speedup | Up to 43x; up to 48x on rejected input | ~9x in Zod's headline example |
30
+ | Uses `new Function()` at runtime | No | Yes |
33
31
  | Cold start | Fast; the validator is already generated | Pays for code generation at startup/first use |
34
32
  | Strict CSP without `'unsafe-eval'` | Supported | Compilation is unavailable |
35
33
  | Compiler shipped in the runtime bundle | No; only validators and runtime helpers ship | Yes; about 7 KB gzipped according to Zod |
36
34
 
37
- \*zod-compiler's optional [`jit()`](#4-runtime-compilation-no-build-step) and
38
- [Node.js register hook](#5-nodejs-register-hook) use `new Function()` and have the same runtime
39
- code-generation and CSP trade-offs as Zod's `z.compile()`.
40
-
41
35
  ## Usage
42
36
 
43
37
  Five ways to use zod-compiler. Pick one:
@@ -135,8 +129,7 @@ Compilation is lazy, costing 0.1-0.3 ms on a schema's first parse. `{ eager: tru
135
129
  and `jitAll(namespace)` takes a whole module.
136
130
 
137
131
  The cost is the import: ~570 KB of codegen and `acorn`, **~10 ms of module load**. That suits a
138
- long-lived process, not a CLI, a cold serverless handler or a browser. Use the build plugin there, and
139
- have libraries ship plain Zod so the app can decide.
132
+ long-lived process, not a CLI, a cold serverless handler or a browser.
140
133
 
141
134
  Needs `new Function`, as Zod's own object fast-path does. `z.config({ jitless: true })` and a CSP
142
135
  that blocks eval both leave a working plain-Zod schema.
@@ -157,11 +150,9 @@ also chains with TypeScript runners:
157
150
  node --import zod-compiler/register --import tsx src/server.ts
158
151
  ```
159
152
 
160
- This is runtime JIT instrumentation, not the AOT source rewriting the build plugins perform. The hook
161
- registers the live Zod objects behind exported schema bindings and generates validators in-process on
162
- first use. It does not execute modules twice, and adds no cache beyond Node's own module cache. Use a
163
- build plugin or the CLI when validator code must exist before Node starts, or when `new Function` is
164
- unavailable at runtime.
153
+ This is runtime JIT instrumentation, not AOT source rewriting: the hook generates validators
154
+ in-process on first use. Use a build plugin or the CLI when validator code must exist before Node
155
+ starts, or when `new Function` is unavailable.
165
156
 
166
157
  Optional settings come from `zod-compiler.json` in the working directory:
167
158
 
@@ -282,10 +273,9 @@ bundle. Schemas in a file that share a structurally identical sub-shape emit its
282
273
  **19-28% raw / 10-18% gzipped** and scaling with how much the file repeats.
283
274
 
284
275
  Build plugins serve that module from a resolve hook (`virtual:zod-compiler/runtime`, or
285
- `__zod-compiler-runtime__` on webpack and rspack, which reject the `virtual:` scheme). A loader host
286
- has no hook, so [Turbopack](#nextjs-turbopack) imports the same code from the real subpath
287
- `zod-compiler/runtime` instead. It is opt-in there, since it only pays off where the host bundles that
288
- import rather than leaving it external.
276
+ `__zod-compiler-runtime__` on webpack and rspack). [Turbopack](#nextjs-turbopack) has no such hook, so
277
+ it imports the real subpath `zod-compiler/runtime` instead, opt-in because it only pays off where the
278
+ host bundles that import.
289
279
 
290
280
  **Transpile-only esbuild builds** (no `--bundle`) never fire the bundler's resolve hooks, so the
291
281
  `virtual:` specifier would survive into `dist/` and fail at runtime. Set `codegenMode: "inline"` to emit
@@ -376,8 +366,7 @@ The step pays for itself: **Hermes ships no JIT and no `new Function`**, so Zod'
376
366
  is unavailable on device and [`jit()`](#4-runtime-compilation-no-build-step) cannot run there at all.
377
367
 
378
368
  Keep schema modules free of `react-native` and `expo-*` imports, transitively. Discovery executes each
379
- file and its import graph in Node (in both modes), and one that throws falls back to runtime Zod
380
- silently.
369
+ file and its import graph in Node, and one that throws falls back to runtime Zod silently.
381
370
 
382
371
  ### Compact Output (`output: "compact"`)
383
372
 
@@ -391,14 +380,13 @@ zodCompiler({ output: "compact" });
391
380
 
392
381
  ### Workers and Serverless Startup
393
382
 
394
- Workers construct every imported schema at module init, even when an isolate validates only a few.
395
- Compiling all of them buys validation speed at the cost of bundle size and startup work. Compact output
396
- trims the generated error path but still retains the Zod schema, and does not make eager construction
397
- lazy.
383
+ Workers construct every imported schema at module init, even when an isolate validates only a few, so
384
+ compiling all of them trades bundle size and startup work for validation speed. Compact output trims
385
+ the generated error path but does not make construction lazy.
398
386
 
399
- If your app has a clear schema boundary, narrow `include` or use `schemas: "explicit"` to skip
400
- intermediate exports. Use `output: "bag"` only where consumers need no Zod APIs (`.shape`, `.extend()`,
401
- `.meta()`, `z.toJSONSchema()`); it can drop the retained schema entirely.
387
+ Narrow `include` or use `schemas: "explicit"` to skip intermediate exports. Use `output: "bag"` where
388
+ consumers need no Zod APIs (`.shape`, `.extend()`, `.meta()`, `z.toJSONSchema()`); it drops the
389
+ retained schema entirely.
402
390
 
403
391
  Measure startup separately from validation throughput, on the target deployment and bundle.
404
392
 
@@ -439,10 +427,9 @@ never mention `zod` cost nothing.
439
427
 
440
428
  ### Parallel Transforms
441
429
 
442
- Discovery runs one file at a time on the bundler's own thread, serializing executions so concurrent
443
- transforms cannot double-execute a shared dependency. `parallel` moves whole transforms onto worker
444
- threads instead, each with its own loader and module cache, which is what makes running them at the
445
- same time sound.
430
+ Discovery runs one file at a time on the bundler's thread so concurrent transforms cannot
431
+ double-execute a shared dependency. `parallel` moves whole transforms onto worker threads, each with
432
+ its own loader and module cache.
446
433
 
447
434
  ```typescript
448
435
  zodCompiler({ parallel: true }); // one worker per core, less one, capped at 4
@@ -459,18 +446,15 @@ win; files chained through each other can lose. Both rows below are 120 files of
459
446
  | independent graphs | 3,633 ms | 2,263 ms | 1,508 ms | 1,786 ms | 2,119 ms |
460
447
  | 120-deep import chain | 945 ms | 977 ms | 1,045 ms | 1,796 ms | 3,332 ms |
461
448
 
462
- So measure before adopting it. `ZOD_COMPILER_TIMING=1` prints the per-phase breakdown, and `discover`
463
- is the line workers move. Throughput peaks around four workers and declines past it: every extra worker
464
- re-executes more graph, holds another copy in memory, and adds to the generated source that the single
465
- receiving thread has to deserialize.
449
+ Measure before adopting it: `ZOD_COMPILER_TIMING=1` prints the per-phase breakdown, and `discover` is
450
+ the line workers move. Throughput peaks around four workers and declines past it, as every extra
451
+ worker re-executes more graph and holds another copy in memory.
466
452
 
467
- Emitted code, sourcemaps and cache entries are identical either way. `parallel` is not part of the
468
- cache key, so parallel and serial builds share one cache. Disk caching and dependency crawling stay on
469
- the bundler thread, and a worker that cannot start or dies mid-build has its file retried in-process
470
- rather than failing the build.
453
+ Emitted code, sourcemaps and cache entries are identical either way, and `parallel` is not part of the
454
+ cache key, so parallel and serial builds share one cache. A worker that cannot start or dies mid-build
455
+ has its file retried in-process.
471
456
 
472
- A **warm cache still beats parallelism** and costs no memory. Reach for `parallel` on the cold runs the
473
- cache cannot help with.
457
+ A **warm cache still beats parallelism** and costs no memory. Reach for `parallel` on cold runs.
474
458
 
475
459
  ## Framework Examples
476
460
 
@@ -545,7 +529,7 @@ npx zod-compiler check src/schemas.ts --json --fail-under 80
545
529
 
546
530
  ## What Gets Compiled
547
531
 
548
- ### Fully Compiled (up to 44x faster)
532
+ ### Fully Compiled (up to 43x faster)
549
533
 
550
534
  Every Zod type except the fallbacks below: all primitives, `object` / `strictObject` / `looseObject`,
551
535
  `array`, `tuple`, `record`, `set`, `map`, `union`, `discriminatedUnion`, `intersection`, `pipe`,
@@ -596,61 +580,63 @@ Schema-level `error` and `z.config()` maps are unaffected; for a per-call map us
596
580
 
597
581
  | Scenario | Zod v3 | Zod v4 | **zod-compiler** | Typia | AJV | vs Zod v4 |
598
582
  | ----------------------------------------------- | ------ | ------ | ---------------- | ----- | ----- | --------- |
599
- | simple string | 12.6M | 15.6M | **16.8M** | 17.2M | 17.4M | 1.1x |
600
- | string (min/max) | 12.4M | 7.4M | **17.4M** | 17.6M | 15.5M | 2.4x |
601
- | number (int+positive) | 11.8M | 9.4M | **17.2M** | 16.3M | 17.4M | 1.8x |
602
- | enum | 12.1M | 14.7M | **17.4M** | 17.4M | 17.2M | 1.2x |
603
- | bigint (min/max) | 11.2M | 7.9M | **16.7M** | — | — | 2.1x |
604
- | tuple [string, int, bool] | 5.6M | 7.3M | **17.4M** | 16.4M | 15.7M | 2.4x |
605
- | record\<string, number\> | 3.1M | 2.6M | **12.7M** | 12.0M | 15.2M | 5.0x |
606
- | set\<string\> (5 items) | 3.7M | 2.3M | **15.2M** | — | — | 6.6x |
607
- | set\<string\> (20 items) | 1.3M | 680K | **12.1M** | — | — | **18x** |
608
- | map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.2M** | — | — | **10x** |
609
- | map\<string, number\> (20 entries) | 637K | 347K | **8.5M** | — | — | **24x** |
610
- | pipe (non-transform) | 8.7M | 4.7M | **17.3M** | — | — | 3.6x |
611
- | discriminatedUnion (3 variants) | 3.3M | 5.2M | **17.2M** | 16.1M | 7.8M | 3.3x |
612
- | discriminatedUnion (8 variants, rotating) | 2.6M | 4.4M | **10.3M** | — | — | 2.3x |
613
- | plain union of 8 tagged objects (auto-discrim.) | 356K | 1.3M | **10.2M** | — | — | 7.7x |
614
- | strict object (DB row) | 1.8M | 3.0M | **11.3M** | — | — | 3.8x |
615
- | medium object (valid) | 1.9M | 2.2M | **9.4M** | 11.2M | 7.5M | 4.2x |
616
- | medium object (extra keys stripped) | 1.8M | 2.0M | **9.8M** | — | — | 4.9x |
617
- | medium object (invalid) | 534K | 372K | **15.5M** | 2.9M | 7.5M | **42x** |
618
- | large object (10 items) | 120K | 163K | **5.1M** | 5.9M | 1.2M | **31x** |
619
- | large object (100 items) | 13K | 17K | **766K** | 1.3M | 127K | **44x** |
620
- | readonly field (wrapper compiles away) | 3.0M | 6.6M | **16.7M** | — | — | 2.5x |
621
- | readonly root object (rebuild + freeze) | 2.8M | 5.5M | **12.9M** | — | — | 2.3x |
583
+ | simple string | 12.7M | 15.1M | **16.5M** | 17.1M | 17.7M | 1.1x |
584
+ | string (min/max) | 12.5M | 7.3M | **16.7M** | 17.0M | 15.0M | 2.3x |
585
+ | number (int+positive) | 12.0M | 9.5M | **16.8M** | 17.0M | 18.0M | 1.8x |
586
+ | enum | 11.6M | 15.1M | **16.7M** | 17.3M | 17.5M | 1.1x |
587
+ | bigint (min/max) | 11.5M | 8.4M | **16.6M** | — | — | 2.0x |
588
+ | tuple [string, int, bool] | 5.7M | 7.6M | **17.5M** | 17.3M | 16.2M | 2.3x |
589
+ | record\<string, number\> | 3.2M | 2.6M | **12.6M** | 11.8M | 15.1M | 4.9x |
590
+ | set\<string\> (5 items) | 3.6M | 2.3M | **15.7M** | — | — | 7.0x |
591
+ | set\<string\> (20 items) | 1.3M | 679K | **12.3M** | — | — | **18x** |
592
+ | map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.4M** | — | — | **10x** |
593
+ | map\<string, number\> (20 entries) | 658K | 348K | **8.7M** | — | — | **25x** |
594
+ | pipe (non-transform) | 8.9M | 4.7M | **17.6M** | — | — | 3.7x |
595
+ | discriminatedUnion (3 variants) | 3.3M | 5.2M | **16.3M** | 15.5M | 7.6M | 3.1x |
596
+ | discriminatedUnion (8 variants, rotating) | 2.6M | 4.6M | **10.1M** | — | — | 2.2x |
597
+ | plain union of 8 tagged objects (auto-discrim.) | 350K | 1.3M | **10.4M** | — | — | 7.8x |
598
+ | strict object (DB row) | 1.8M | 3.0M | **11.5M** | — | — | 3.9x |
599
+ | medium object (valid) | 2.0M | 2.3M | **9.8M** | 11.4M | 7.6M | 4.2x |
600
+ | medium object (extra keys stripped) | 1.8M | 2.1M | **9.6M** | — | — | 4.6x |
601
+ | medium object (invalid) | 464K | 370K | **16.0M** | 2.9M | 7.6M | **43x** |
602
+ | large object (10 items) | 119K | 169K | **5.2M** | 5.9M | 1.2M | **31x** |
603
+ | large object (100 items) | 13K | 18K | **763K** | 1.3M | 127K | **43x** |
604
+ | readonly field (wrapper compiles away) | 3.1M | 6.6M | **17.1M** | — | — | 2.6x |
605
+ | readonly root object (rebuild + freeze) | 2.8M | 5.6M | **13.0M** | — | — | 2.3x |
622
606
  | readonly array (delegates to Zod) | 3.9M | 4.3M | **4.2M** | — | — | 1.0x |
623
- | recursive tree (7 nodes) | 581K | 1.0M | **7.5M** | 11.6M | 4.7M | 7.4x |
624
- | recursive tree (121 nodes) | 31K | 57K | **777K** | 1.9M | 371K | **14x** |
625
- | nested recursion (7 nodes) | 395K | 683K | **7.9M** | 11.2M | 3.0M | **12x** |
626
- | nested recursion (121 nodes) | 24K | 42K | **805K** | 1.6M | 204K | **19x** |
627
- | deeply nested object (243 leaves) | 11K | 27K | **825K** | 1.0M | 125K | **30x** |
628
- | event log (combined) | 363K | 795K | **7.3M** | — | — | 9.2x |
629
- | object with transform (zero-capture) | 1.1M | 1.9M | **6.7M** | — | — | 3.5x |
630
- | array 10 × transform (zero-capture) | 121K | 206K | **4.1M** | — | — | **20x** |
631
- | array 50 × transform (zero-capture) | 26K | 41K | **1.0M** | — | — | **25x** |
632
- | object with captured transform | 1.2M | 7.9M | **15.9M** | — | — | 2.0x |
633
- | object with captured refine (cross-field) | 1.4M | 2.2M | **11.5M** | — | — | 5.2x |
634
- | object with superRefine (cross-field) | 1.4M | 2.1M | **9.2M** | — | — | 4.3x |
635
- | coerced query object (valid) | 1.8M | 2.9M | **5.3M** | — | — | 1.9x |
636
- | coerced query object (invalid) | 1.0M | 826K | **10.3M** | — | — | **12x** |
637
- | preprocessed query object (valid) | 433K | 1.7M | **5.3M** | — | — | 3.2x |
638
- | preprocessed query object (invalid) | 392K | 782K | **12.3M** | — | — | **16x** |
639
- | stringbool config object (valid) | — | 2.8M | **6.3M** | — | — | 2.2x |
640
- | stringbool config object (invalid) | — | 682K | **13.2M** | — | — | **19x** |
641
- | custom/instanceof request (valid) | 965K | 3.1M | **9.9M** | — | — | 3.2x |
642
- | custom/instanceof request (invalid) | 786K | 928K | **13.2M** | — | — | **14x** |
643
- | disjoint object intersection (valid) | 1.4M | 1.6M | **9.5M** | — | — | 5.9x |
644
- | disjoint object intersection (invalid) | 488K | 331K | **15.3M** | — | — | **46x** |
607
+ | recursive tree (7 nodes) | 576K | 1.0M | **7.8M** | 11.9M | 4.8M | 7.8x |
608
+ | recursive tree (121 nodes) | 32K | 57K | **774K** | 1.9M | 371K | **13x** |
609
+ | nested recursion (7 nodes) | 388K | 677K | **7.7M** | 10.4M | 3.0M | **11x** |
610
+ | nested recursion (121 nodes) | 24K | 41K | **804K** | 1.6M | 210K | **20x** |
611
+ | deeply nested object (243 leaves) | 11K | 27K | **847K** | 1.0M | 122K | **31x** |
612
+ | event log (combined) | 370K | 786K | **7.7M** | — | — | 9.8x |
613
+ | catalog product with image URLs (valid) | 426K | 451K | **1.4M** | — | — | 3.1x |
614
+ | catalog product with image URLs (invalid) | 115K | 104K | **237K** | — | — | 2.3x |
615
+ | catalog page (20 products) | 21K | 23K | **74K** | — | — | 3.2x |
616
+ | object with transform (zero-capture) | 1.1M | 1.9M | **7.3M** | — | — | 3.9x |
617
+ | array 10 × transform (zero-capture) | 120K | 209K | **4.3M** | — | — | **21x** |
618
+ | array 50 × transform (zero-capture) | 25K | 43K | **1.0M** | — | — | **24x** |
619
+ | object with captured transform | 1.2M | 7.8M | **15.3M** | — | — | 2.0x |
620
+ | object with captured refine (cross-field) | 1.5M | 2.2M | **11.2M** | — | — | 5.1x |
621
+ | object with superRefine (cross-field) | 1.4M | 2.1M | **9.1M** | — | — | 4.3x |
622
+ | coerced query object (valid) | 1.8M | 2.9M | **5.4M** | — | — | 1.8x |
623
+ | coerced query object (invalid) | 1.0M | 843K | **10.2M** | — | — | **12x** |
624
+ | preprocessed query object (valid) | 430K | 1.7M | **5.3M** | — | — | 3.1x |
625
+ | preprocessed query object (invalid) | 382K | 780K | **12.3M** | — | — | **16x** |
626
+ | stringbool config object (valid) | — | 2.8M | **6.4M** | — | — | 2.2x |
627
+ | stringbool config object (invalid) | — | 688K | **12.6M** | — | — | **18x** |
628
+ | custom/instanceof request (valid) | 972K | 3.1M | **10.3M** | — | — | 3.3x |
629
+ | custom/instanceof request (invalid) | 776K | 945K | **13.5M** | — | — | **14x** |
630
+ | disjoint object intersection (valid) | 1.4M | 1.6M | **10.0M** | — | — | 6.1x |
631
+ | disjoint object intersection (invalid) | 482K | 333K | **16.0M** | — | — | **48x** |
645
632
 
646
633
  _ops/s, higher is better. `vp test bench` on an Apple M4 Max (zod 4.5.2, zod v3 3.23.8, typia 12, ajv 8),
647
634
  best of three runs. The harness costs ~60 ns per iteration, so the fastest rows sit at that floor and gaps
648
635
  between the AOT columns there are noise, not real._
649
636
 
650
637
  Nested objects, arrays and recursive types gain the most. Rejection is fast because a failed
651
- `safeParse` defers building the error until `.error` is read. Zod 4.5 stopped capturing a stack trace on
652
- that path too, so its own rejected-input rows are several times faster than 4.3's and the gap there is
653
- narrower than it was.
638
+ `safeParse` defers building the error until `.error` is read. Zod 4.5 stopped capturing a stack trace
639
+ there too, narrowing the gap on rejected input.
654
640
 
655
641
  ```bash
656
642
  vp run benchmark # run locally
@@ -658,29 +644,22 @@ vp run benchmark # run locally
658
644
 
659
645
  ### Performance Architecture
660
646
 
661
- An eligible schema compiles to a **fast path**, one `&&` chain validating the whole input with zero
662
- allocations and reused by `.is()` and `parse()`, plus a **slow path** that collects errors, runs only on
663
- failure, and is deferred until `.error` is read. A `z.object()` strips, so it instead compiles to a
664
- single pass that validates and rebuilds together and bails on the first failure, covering the reshaping
665
- idioms too (array size checks, `.refine()`, `.default()`, `.trim()`, `.transform()`).
666
-
667
- Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first on both passes (so a
668
- payload with a wrong boolean is rejected before its email is scanned, whichever was declared first),
669
- discriminated unions dispatch through a `switch` on the tag (plain tagged unions are auto-discriminated
670
- into it, on the stripping pass as well as the fast check, and the selected option re-checks neither
671
- object-ness nor the tag), and oversized check functions are split to stay within V8's optimizer budget.
672
- `.is()` stays a zero-allocation predicate on schemas with `.default()` fields. `z.email()` runs as a single linear scan instead of a backtracking regex, a record's
673
- plain-object guard exits on one comparison for an ordinary object, and a case-insensitive `stringbool`
674
- looks its input up verbatim before paying for `toLowerCase()`. Stripping objects, native coercions,
675
- `stringbool`, defaults, string rewrites, context-free preprocessors and synchronous transforms validate
676
- and build their output in one pass. An intersection of two objects with disjoint keys compiles to that
677
- same single pass over the merged shape, and `z.custom()` / `z.instanceof()` compile to a direct predicate
678
- call.
679
-
680
- Where success is cheaper to compile than failure, only the verdict and output are compiled.
681
- Intersections and `custom` keep the original Zod schema to construct issues, so a rejection still
682
- reports exactly what Zod would, an intersection's one-issue-per-side shape included, without slowing the
683
- hot path.
647
+ A schema compiles to a **fast path** — one zero-allocation `&&` chain, shared by `.is()` and
648
+ `parse()` — plus a **slow path** that collects errors only on failure, deferred until `.error` is read.
649
+ Schemas that reshape their input (stripping objects, coercions, `stringbool`, defaults, transforms,
650
+ context-free preprocessors, disjoint-key intersections) instead validate and rebuild in a single pass
651
+ that bails on the first failure.
652
+
653
+ Other optimizations:
654
+
655
+ - Regexes pre-compiled, bounded repeats unrolled; `z.email()` is a linear scan, not a backtracking regex.
656
+ - Checks run cheapest-first, regardless of declaration order.
657
+ - Discriminated unions dispatch through a `switch`; plain tagged unions are auto-discriminated into one.
658
+ - `z.custom()` / `z.instanceof()` compile to a direct predicate call.
659
+ - Oversized check functions are split to stay within V8's optimizer budget.
660
+
661
+ Intersections and `custom` keep the original Zod schema to construct issues, so rejections match Zod
662
+ exactly without slowing the hot path.
684
663
 
685
664
  ## Development
686
665
 
@@ -1,5 +1,5 @@
1
- import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRuntimeHelper, emitTemp, escapeString, hasMutation, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput } from "./context.js";
2
1
  import { ZC_ASYNC_DECL, ZC_HOP_DECL, ZC_PLAIN_DECL, ZC_PROTO_SCRUB_DECL } from "./issue-decls.js";
2
+ import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRuntimeHelper, emitTemp, escapeString, hasMutation, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput } from "./context.js";
3
3
  import { defaultValueExpr, needsPostInnerDefault } from "./schemas/default.js";
4
4
  import { detectUnionDiscriminator } from "./schemas/discriminated-union.js";
5
5
  import { estimateFastCost, orderByRuntimeCost, predictedInlineSize } from "./fast-size.js";
@@ -501,14 +501,15 @@ declare function emitRuntimeHelper(ctx: CodeGenContext, name: string, decl: stri
501
501
  * `[]` root, so any path that looks like an array literal IS one — the new
502
502
  * segment is spliced in to keep issue paths a single array allocation
503
503
  * (`["data","items",__i_7]`) instead of an allocation per nesting level
504
- * (`["data"].concat("items").concat(__i_7)`). Opaque expressions fall back
505
- * to .concat().
504
+ * (`["data"].concat("items").concat(__i_7)`). An opaque path — a shared walk's
505
+ * `path` parameter, or an extension of it — appends through `__zcPa` (see
506
+ * ZC_PATH_APPEND_DECL), not `.concat()`.
506
507
  */
507
- declare function extendPath(parentPath: string, segExpr: string): string;
508
+ declare function extendPath(ctx: CodeGenContext, parentPath: string, segExpr: string): string;
508
509
  /** Extend a path expression with a static string key. */
509
- declare function extendStaticPath(parentPath: string, key: string): string;
510
+ declare function extendStaticPath(ctx: CodeGenContext, parentPath: string, key: string): string;
510
511
  /** Extend a path expression with a numeric index. */
511
- declare function extendStaticPathIndex(parentPath: string, index: number): string;
512
+ declare function extendStaticPathIndex(ctx: CodeGenContext, parentPath: string, index: number): string;
512
513
  /**
513
514
  * Slow-walk one member of a container — an array element, a tuple slot, a
514
515
  * record value — without writing to the container while it is still the
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","names":[],"sources":["../../../src/core/codegen/context.ts"],"mappings":";;;;KAgBY;;;;;KAMA;;;;;;;;;;;;;;;;UAiBK;WACN,MAAM;WACN;WACA;;UAGM;EACf;EACA;;EAEA;;;;;;EAMA,aAAa;;;;;;;EAOb;;;;;;;;;;EAUA;;;;;;;EAOA;;;;;;;EAOA;;;UAIe;;EAEf;;;;;EAKA;;;;;;EAMA;;EAEA,QAAQ;;;UAIO;EACf;EACA;EACA;;EAEA,YAAY;;EAEZ,MAAM;;EAEN,aAAa;;;;;;;;EAQb;;EAEA;;;;;;;;;EASA,aAAa,YAAY;;EAEzB,gBAAgB;;EAEhB,aAAa;;EAEb,eAAe,UAAU;;EAEzB,sBAAsB;;EAEtB;;EAEA,gBAAgB;;EAEhB,gBAAgB,QAAQ;;EAExB,uBAAuB,QAAQ;;;;;;EAM/B,gBAAgB;;;UAMD;WACN;WACA;WACA;WACA;WACA,KAAK;;;;;;;WAOL;;;;;;;;;;;;;;;;WAiBA;;;;;;;;;EAUT,MACE,IAAI,UACJ;IACE;IACA;IACA;IACA;IAGA;;;EAKJ,KAAK;;EAGL,MAAM,gBAAgB,iBAAiB;;EAGvC,IAAI,gBAAgB;;;KAIV,cAAc,UAAU,WAAW,aAAa,IAAI,GAAG,GAAG;;;;;;UASrD;EACf;;;;;;;;;;;EAWA;;;iBAIc,iBAAiB,OAAO;;;;;;;;;;;;;;;;;;;;iBAuBxB,oBACd,GAAG,SACH,SAAS,UACT,kBACA;;UAkBe;WACN;WACA,KAAK;;;;;;;WAQL;;WAGA,OAAO;;;;;;;WAQP;;;;;;;;;;;WAYA;;;;;EAMT,MAAM,IAAI,UAAU;IAAc;IAAgB;;;;;;;;;EASlD,OAAO,gBAAgB;;EAGvB,KAAK;;;;;;;EAQL,MAAM;;EAGN,MAAM,gBAAgB,iBAAiB;;;KAI7B,cAAc,UAAU,WAAW,aAAa,IAAI,GAAG,GAAG;;iBAKtD,SAAS,KAAK,gBAAgB;;;;;;;;;;;;;iBAgB9B,aAAa,KAAK,gBAAgB;;;;;;;;;;;iBAoBlC,mBACd,KAAK,gBACL;EAAU;EAA+B;;;;;;;;;;;;;;;;;;;iBA0B3B,eAAe,KAAK,gBAAgB;;;;;;;;;iBAiBpC,UAAU,KAAK,gBAAgB;;;;;;;;;;;;;cAqBlC;;;;;;;;iBASG,mBAAmB,KAAK;;;;;;;;;;;;;iBA0CxB,UACd,KAAK,gBACL,gBACA,iBACA;;;;;;;;iBAiCc,sBAAsB,KAAK,gBAAgB;;;;;;;;;;;;iBA4B3C,aAAa,KAAK,gBAAgB,gBAAgB;;;;;;;;;;;iBAoBlD,mBACd,KAAK,gBACL,MAAM,cACN,qBACA;;iBAUc,QAAQ,KAAK,gBAAgB,gBAAgB;;;;;;;;;;;;;;;;;;;;cAwBhD;;;;;;;;;;iBAWG,kBACd,KAAK,gBACL,yBACA;;;;;;;;cAgBW;iBA8BG,aAAa;;KAKjB;;;;;;;;;;;;;;;iBAgBI,cAAc,GAAG,eAAe,KAAK;;;;;;;;;;iBAerC,YAAY,GAAG;;;;;;iBA+Bf,kBAAkB,KAAK,gBAAgB,cAAc;;;;;;;;;;;;iBAqBrD,WAAW,oBAAoB;;iBAS/B,iBAAiB,oBAAoB;;iBAKrC,sBAAsB,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC1C,YACd,GAAG,SACH,IAAI,UACJ;WACW;WACA;WACA;WACA;WACA;WACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuDG,wBAAwB,IAAI;EAAa;;;;;;;;;;;iBAmBzC,gBAAgB,IAAI;iBAepB,YAAY,IAAI;;;;;;;;;;;;;;iBAuFhB,oBAAoB,IAAI;;;;;;;;;;iBAaxB,iBAAiB,IAAI;;;;;iBAiDrB,cACd,GAAG,UAAU,gBAAgB,cAAc,YAC3C,GAAG,UAAU,gBAAgB,cAAc"}
1
+ {"version":3,"file":"context.d.ts","names":[],"sources":["../../../src/core/codegen/context.ts"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { ZC_PATH_APPEND_DECL } from "./issue-decls.js";
1
2
  import { fastTestSource, lookupWellKnownRegex, wellKnownRegexSourceName } from "./well-known-regex.js";
2
3
  //#region src/core/codegen/context.ts
3
4
  /** `var a,b;` declaration for a scope's temps, or "" when it allocated none. */
@@ -392,21 +393,22 @@ function emitRuntimeHelper(ctx, name, decl) {
392
393
  * `[]` root, so any path that looks like an array literal IS one — the new
393
394
  * segment is spliced in to keep issue paths a single array allocation
394
395
  * (`["data","items",__i_7]`) instead of an allocation per nesting level
395
- * (`["data"].concat("items").concat(__i_7)`). Opaque expressions fall back
396
- * to .concat().
396
+ * (`["data"].concat("items").concat(__i_7)`). An opaque path — a shared walk's
397
+ * `path` parameter, or an extension of it — appends through `__zcPa` (see
398
+ * ZC_PATH_APPEND_DECL), not `.concat()`.
397
399
  */
398
- function extendPath(parentPath, segExpr) {
400
+ function extendPath(ctx, parentPath, segExpr) {
399
401
  if (parentPath === "[]") return `[${segExpr}]`;
400
402
  if (parentPath.startsWith("[") && parentPath.endsWith("]")) return `${parentPath.slice(0, -1)},${segExpr}]`;
401
- return `${parentPath}.concat(${segExpr})`;
403
+ return `${emitRuntimeHelper(ctx, "__zcPa", ZC_PATH_APPEND_DECL)}(${parentPath},${segExpr})`;
402
404
  }
403
405
  /** Extend a path expression with a static string key. */
404
- function extendStaticPath(parentPath, key) {
405
- return extendPath(parentPath, escapeString(key));
406
+ function extendStaticPath(ctx, parentPath, key) {
407
+ return extendPath(ctx, parentPath, escapeString(key));
406
408
  }
407
409
  /** Extend a path expression with a numeric index. */
408
- function extendStaticPathIndex(parentPath, index) {
409
- return extendPath(parentPath, String(index));
410
+ function extendStaticPathIndex(ctx, parentPath, index) {
411
+ return extendPath(ctx, parentPath, String(index));
410
412
  }
411
413
  /**
412
414
  * Slow-walk one member of a container — an array element, a tuple slot, a