zod-compiler 2.0.4 → 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.
- package/README.md +97 -118
- package/dist/core/codegen/build-path.js +1 -1
- package/dist/core/codegen/context.d.ts +44 -6
- package/dist/core/codegen/context.d.ts.map +1 -1
- package/dist/core/codegen/context.js +61 -9
- package/dist/core/codegen/context.js.map +1 -1
- package/dist/core/codegen/index.js +1 -1
- package/dist/core/codegen/issue-decls.d.ts +18 -1
- package/dist/core/codegen/issue-decls.d.ts.map +1 -1
- package/dist/core/codegen/issue-decls.js +19 -1
- package/dist/core/codegen/issue-decls.js.map +1 -1
- package/dist/core/codegen/schemas/array.d.ts.map +1 -1
- package/dist/core/codegen/schemas/array.js +24 -18
- package/dist/core/codegen/schemas/array.js.map +1 -1
- package/dist/core/codegen/schemas/custom.js +1 -1
- package/dist/core/codegen/schemas/discriminated-union.d.ts.map +1 -1
- package/dist/core/codegen/schemas/discriminated-union.js +3 -3
- package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
- package/dist/core/codegen/schemas/effect.d.ts.map +1 -1
- package/dist/core/codegen/schemas/effect.js +3 -3
- package/dist/core/codegen/schemas/effect.js.map +1 -1
- package/dist/core/codegen/schemas/fallback.js +1 -1
- package/dist/core/codegen/schemas/map.d.ts.map +1 -1
- package/dist/core/codegen/schemas/map.js +71 -18
- package/dist/core/codegen/schemas/map.js.map +1 -1
- package/dist/core/codegen/schemas/number.js +1 -1
- package/dist/core/codegen/schemas/object.js +3 -3
- package/dist/core/codegen/schemas/object.js.map +1 -1
- package/dist/core/codegen/schemas/record.d.ts.map +1 -1
- package/dist/core/codegen/schemas/record.js +25 -19
- package/dist/core/codegen/schemas/record.js.map +1 -1
- package/dist/core/codegen/schemas/set.d.ts.map +1 -1
- package/dist/core/codegen/schemas/set.js +51 -13
- package/dist/core/codegen/schemas/set.js.map +1 -1
- package/dist/core/codegen/schemas/sizeable.js +1 -1
- package/dist/core/codegen/schemas/string.js +1 -1
- package/dist/core/codegen/schemas/tuple.d.ts.map +1 -1
- package/dist/core/codegen/schemas/tuple.js +16 -13
- package/dist/core/codegen/schemas/tuple.js.map +1 -1
- package/dist/core/codegen/schemas/union.d.ts.map +1 -1
- package/dist/core/codegen/schemas/union.js +22 -16
- package/dist/core/codegen/schemas/union.js.map +1 -1
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1 -0
- package/dist/unplugin/virtual.js +1 -1
- 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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
32
|
-
| Uses `new Function()` at runtime
|
|
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.
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
re-executes more graph
|
|
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
|
|
468
|
-
cache key, so parallel and serial builds share one cache.
|
|
469
|
-
|
|
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
|
|
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
|
|
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.
|
|
600
|
-
| string (min/max) | 12.
|
|
601
|
-
| number (int+positive) |
|
|
602
|
-
| enum |
|
|
603
|
-
| bigint (min/max) | 11.
|
|
604
|
-
| tuple [string, int, bool] | 5.
|
|
605
|
-
| record\<string, number\> | 3.
|
|
606
|
-
| set\<string\> (5 items) | 3.
|
|
607
|
-
| set\<string\> (20 items) | 1.3M |
|
|
608
|
-
| map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.
|
|
609
|
-
| map\<string, number\> (20 entries) |
|
|
610
|
-
| pipe (non-transform) | 8.
|
|
611
|
-
| discriminatedUnion (3 variants) | 3.3M | 5.2M | **
|
|
612
|
-
| discriminatedUnion (8 variants, rotating) | 2.6M | 4.
|
|
613
|
-
| plain union of 8 tagged objects (auto-discrim.) |
|
|
614
|
-
| strict object (DB row) | 1.8M | 3.0M | **11.
|
|
615
|
-
| medium object (valid) |
|
|
616
|
-
| medium object (extra keys stripped) | 1.8M | 2.
|
|
617
|
-
| medium object (invalid) |
|
|
618
|
-
| large object (10 items) |
|
|
619
|
-
| large object (100 items) | 13K |
|
|
620
|
-
| readonly field (wrapper compiles away) | 3.
|
|
621
|
-
| readonly root object (rebuild + freeze) | 2.8M | 5.
|
|
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) |
|
|
624
|
-
| recursive tree (121 nodes) |
|
|
625
|
-
| nested recursion (7 nodes) |
|
|
626
|
-
| nested recursion (121 nodes) | 24K |
|
|
627
|
-
| deeply nested object (243 leaves) | 11K | 27K | **
|
|
628
|
-
| event log (combined) |
|
|
629
|
-
|
|
|
630
|
-
|
|
|
631
|
-
|
|
|
632
|
-
| object with
|
|
633
|
-
|
|
|
634
|
-
|
|
|
635
|
-
|
|
|
636
|
-
|
|
|
637
|
-
|
|
|
638
|
-
|
|
|
639
|
-
|
|
|
640
|
-
|
|
|
641
|
-
|
|
|
642
|
-
|
|
|
643
|
-
|
|
|
644
|
-
|
|
|
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
|
|
652
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
`
|
|
676
|
-
|
|
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,52 @@ 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)`).
|
|
505
|
-
*
|
|
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;
|
|
513
|
+
/**
|
|
514
|
+
* Slow-walk one member of a container — an array element, a tuple slot, a
|
|
515
|
+
* record value — without writing to the container while it is still the
|
|
516
|
+
* caller's.
|
|
517
|
+
*
|
|
518
|
+
* Visiting the container's own slot (`input[i]`) as both input and output let
|
|
519
|
+
* every node that writes its output back write into the CALLER's container: a
|
|
520
|
+
* pass-through loose object, a record or a tuple assigns its result
|
|
521
|
+
* unconditionally, so a frozen input (`Object.freeze`, Immer or Redux state)
|
|
522
|
+
* made `safeParse` throw "Cannot assign to read only property" on valid and
|
|
523
|
+
* invalid input alike, and an ordinary input had a replacement — a
|
|
524
|
+
* `__proto__`-scrubbed copy, a recursive member rebuilt by its own validator —
|
|
525
|
+
* swapped into it in place.
|
|
526
|
+
*
|
|
527
|
+
* So a member that rewrites nothing is read into a local and handed a local of
|
|
528
|
+
* its own to write to. A replacement lands on `container`, the binding the walk
|
|
529
|
+
* hands back as its output: it starts as the caller's `original` and becomes
|
|
530
|
+
* `copy` the first time a member's output is not the value read, so a container
|
|
531
|
+
* nothing replaces still comes back by reference (slowObject's pass-through
|
|
532
|
+
* `handoff` does the same for properties). A member whose code never names its
|
|
533
|
+
* output wrote nothing back, and gets no handoff at all.
|
|
534
|
+
*
|
|
535
|
+
* A REWRITING member keeps the slot for both, as before: its later checks read
|
|
536
|
+
* the rewritten value back through the expression it wrote — `.trim().min(1)`
|
|
537
|
+
* measures the trimmed string, a coercion checks the converted value — which
|
|
538
|
+
* two locals would split, checking the original instead. Writing the slot is
|
|
539
|
+
* safe only because every caller copies its container up front for a member
|
|
540
|
+
* {@link hasMutation} reports, so by then the container is the walk's own.
|
|
541
|
+
*/
|
|
542
|
+
declare function visitMember(g: SlowGen, ir: SchemaIR, member: {
|
|
543
|
+
readonly container: string;
|
|
544
|
+
readonly original: string;
|
|
545
|
+
readonly copy: string;
|
|
546
|
+
readonly key: string;
|
|
547
|
+
readonly path: string;
|
|
548
|
+
readonly issues: string;
|
|
549
|
+
}): string;
|
|
512
550
|
/**
|
|
513
551
|
* Check if a SchemaIR tree produces output that is not the input itself —
|
|
514
552
|
* either value-mutating operations (coerce, default, catch, overwrite) that
|
|
@@ -579,5 +617,5 @@ declare function rejectsUndefined(ir: SchemaIR): boolean;
|
|
|
579
617
|
*/
|
|
580
618
|
declare function checkPriority(a: CheckIR | BigIntCheckIR | DateCheckIR | SetCheckIR, b: CheckIR | BigIntCheckIR | DateCheckIR | SetCheckIR): number;
|
|
581
619
|
//#endregion
|
|
582
|
-
export { CodeGenContext, CodeGenResult, CodegenMode, ConstantKind, ENUM_INLINE_THRESHOLD, FastGen, FastGenerator, FastScope, GeneratedConstant, KEY_MEMBERSHIP_INLINE_THRESHOLD, RETAINED_SCHEMA_VAR, RecTargetGen, SlowGen, SlowGenerator, SourceFormLiteral, checkPriority, declareFastTemps, emitConstant, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRegex, emitRegexSourceString, emitRetainedMethod, emitRfDelegate, emitRfZod, emitRuntimeHelper, emitSet, emitTemp, escapeString, extendPath, extendStaticPath, extendStaticPathIndex, fastSentinelWrapper, hasMutation, hasSourceForm, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput };
|
|
620
|
+
export { CodeGenContext, CodeGenResult, CodegenMode, ConstantKind, ENUM_INLINE_THRESHOLD, FastGen, FastGenerator, FastScope, GeneratedConstant, KEY_MEMBERSHIP_INLINE_THRESHOLD, RETAINED_SCHEMA_VAR, RecTargetGen, SlowGen, SlowGenerator, SourceFormLiteral, checkPriority, declareFastTemps, emitConstant, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRegex, emitRegexSourceString, emitRetainedMethod, emitRfDelegate, emitRfZod, emitRuntimeHelper, emitSet, emitTemp, escapeString, extendPath, extendStaticPath, extendStaticPathIndex, fastSentinelWrapper, hasMutation, hasSourceForm, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput, visitMember };
|
|
583
621
|
//# sourceMappingURL=context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","names":[],"sources":["../../../src/core/codegen/context.ts"],"mappings":";;;;
|
|
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,72 @@ 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)`).
|
|
396
|
-
*
|
|
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 `${
|
|
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));
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Slow-walk one member of a container — an array element, a tuple slot, a
|
|
415
|
+
* record value — without writing to the container while it is still the
|
|
416
|
+
* caller's.
|
|
417
|
+
*
|
|
418
|
+
* Visiting the container's own slot (`input[i]`) as both input and output let
|
|
419
|
+
* every node that writes its output back write into the CALLER's container: a
|
|
420
|
+
* pass-through loose object, a record or a tuple assigns its result
|
|
421
|
+
* unconditionally, so a frozen input (`Object.freeze`, Immer or Redux state)
|
|
422
|
+
* made `safeParse` throw "Cannot assign to read only property" on valid and
|
|
423
|
+
* invalid input alike, and an ordinary input had a replacement — a
|
|
424
|
+
* `__proto__`-scrubbed copy, a recursive member rebuilt by its own validator —
|
|
425
|
+
* swapped into it in place.
|
|
426
|
+
*
|
|
427
|
+
* So a member that rewrites nothing is read into a local and handed a local of
|
|
428
|
+
* its own to write to. A replacement lands on `container`, the binding the walk
|
|
429
|
+
* hands back as its output: it starts as the caller's `original` and becomes
|
|
430
|
+
* `copy` the first time a member's output is not the value read, so a container
|
|
431
|
+
* nothing replaces still comes back by reference (slowObject's pass-through
|
|
432
|
+
* `handoff` does the same for properties). A member whose code never names its
|
|
433
|
+
* output wrote nothing back, and gets no handoff at all.
|
|
434
|
+
*
|
|
435
|
+
* A REWRITING member keeps the slot for both, as before: its later checks read
|
|
436
|
+
* the rewritten value back through the expression it wrote — `.trim().min(1)`
|
|
437
|
+
* measures the trimmed string, a coercion checks the converted value — which
|
|
438
|
+
* two locals would split, checking the original instead. Writing the slot is
|
|
439
|
+
* safe only because every caller copies its container up front for a member
|
|
440
|
+
* {@link hasMutation} reports, so by then the container is the walk's own.
|
|
441
|
+
*/
|
|
442
|
+
function visitMember(g, ir, member) {
|
|
443
|
+
const { container, path, issues } = member;
|
|
444
|
+
const slot = `${container}[${member.key}]`;
|
|
445
|
+
if (hasMutation(ir)) return g.visit(ir, {
|
|
446
|
+
input: slot,
|
|
447
|
+
output: slot,
|
|
448
|
+
path,
|
|
449
|
+
issues
|
|
450
|
+
});
|
|
451
|
+
const value = g.temp("mv");
|
|
452
|
+
const out = g.temp("mo");
|
|
453
|
+
const code = g.visit(ir, {
|
|
454
|
+
input: value,
|
|
455
|
+
output: out,
|
|
456
|
+
path,
|
|
457
|
+
issues
|
|
458
|
+
});
|
|
459
|
+
const read = `var ${value}=${slot};`;
|
|
460
|
+
if (!code.includes(out)) return read + code;
|
|
461
|
+
return `${read}var ${out}=${value};${code}if(${out}!==${value}){if(${container}===${member.original}){${container}=${member.copy};}${slot}=${out};}`;
|
|
410
462
|
}
|
|
411
463
|
/**
|
|
412
464
|
* A superRefine callback receives zod's payload, whose `value` is public,
|
|
@@ -564,6 +616,6 @@ function checkPriority(a, b) {
|
|
|
564
616
|
return (CHECK_PRIORITY[a.kind] ?? 99) - (CHECK_PRIORITY[b.kind] ?? 99);
|
|
565
617
|
}
|
|
566
618
|
//#endregion
|
|
567
|
-
export { ENUM_INLINE_THRESHOLD, KEY_MEMBERSHIP_INLINE_THRESHOLD, RETAINED_SCHEMA_VAR, checkPriority, declareFastTemps, emitConstant, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRegex, emitRegexSourceString, emitRetainedMethod, emitRfDelegate, emitRfZod, emitRuntimeHelper, emitSet, emitTemp, escapeString, extendPath, extendStaticPath, extendStaticPathIndex, fastSentinelWrapper, hasMutation, hasSourceForm, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput };
|
|
619
|
+
export { ENUM_INLINE_THRESHOLD, KEY_MEMBERSHIP_INLINE_THRESHOLD, RETAINED_SCHEMA_VAR, checkPriority, declareFastTemps, emitConstant, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRegex, emitRegexSourceString, emitRetainedMethod, emitRfDelegate, emitRfZod, emitRuntimeHelper, emitSet, emitTemp, escapeString, extendPath, extendStaticPath, extendStaticPathIndex, fastSentinelWrapper, hasMutation, hasSourceForm, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput, visitMember };
|
|
568
620
|
|
|
569
621
|
//# sourceMappingURL=context.js.map
|