zod-compiler 2.0.5 → 2.0.7
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.d.ts.map +1 -1
- package/dist/core/codegen/build-path.js +16 -7
- package/dist/core/codegen/build-path.js.map +1 -1
- package/dist/core/codegen/context.d.ts +10 -7
- package/dist/core/codegen/context.d.ts.map +1 -1
- package/dist/core/codegen/context.js +10 -8
- package/dist/core/codegen/context.js.map +1 -1
- package/dist/core/codegen/index.js +3 -2
- package/dist/core/codegen/index.js.map +1 -1
- package/dist/core/codegen/issue-decls.d.ts +28 -1
- package/dist/core/codegen/issue-decls.d.ts.map +1 -1
- package/dist/core/codegen/issue-decls.js +30 -1
- package/dist/core/codegen/issue-decls.js.map +1 -1
- package/dist/core/codegen/schemas/array.js +2 -2
- 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.js +1 -1
- package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
- package/dist/core/codegen/schemas/effect.js +2 -2
- 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.js +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.js +2 -2
- package/dist/core/codegen/schemas/record.js.map +1 -1
- package/dist/core/codegen/schemas/sizeable.js +1 -1
- package/dist/core/codegen/schemas/string.d.ts +15 -2
- package/dist/core/codegen/schemas/string.d.ts.map +1 -1
- package/dist/core/codegen/schemas/string.js +97 -35
- package/dist/core/codegen/schemas/string.js.map +1 -1
- package/dist/core/codegen/schemas/tuple.js +2 -2
- package/dist/core/codegen/schemas/tuple.js.map +1 -1
- package/dist/core/codegen/schemas/union.js +1 -1
- package/dist/core/iife.d.ts +11 -12
- package/dist/core/iife.d.ts.map +1 -1
- package/dist/core/iife.js +12 -13
- package/dist/core/iife.js.map +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +3 -1
- package/dist/static-filter.d.ts +19 -1
- package/dist/static-filter.d.ts.map +1 -1
- package/dist/static-filter.js +27 -1
- package/dist/static-filter.js.map +1 -1
- 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 **140x** 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 140x 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`,
|
|
@@ -564,7 +548,7 @@ A schema delegates to Zod when it reaches JavaScript the generated code cannot r
|
|
|
564
548
|
| ---------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
565
549
|
| `.check(fn)`, `superRefine` + later checks | The callback holds Zod's payload unmediated, or `fatal` aborts Zod's chain |
|
|
566
550
|
| `ctx`-taking or `async` callbacks | Needs Zod's parse context / the async pipeline |
|
|
567
|
-
| `z.
|
|
551
|
+
| `z.jwt()` | Algorithmic format (signature parsing) |
|
|
568
552
|
| Overlapping or policy-sensitive object intersections | Zod's independent parse-and-merge semantics cannot be safely collapsed |
|
|
569
553
|
| `.readonly()` over a pass-through container | Zod freezes the output it rebuilt; these are the caller's own input |
|
|
570
554
|
| Dynamic error maps, unresolvable `z.lazy()` | Not knowable at build time |
|
|
@@ -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 |
|
|
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.
|
|
612
|
-
| discriminatedUnion (8 variants, rotating) | 2.6M | 4.
|
|
613
|
-
| plain union of 8 tagged objects (auto-discrim.) |
|
|
614
|
-
| strict object (DB row) | 1.8M |
|
|
615
|
-
| medium object (valid) | 1.9M | 2.
|
|
616
|
-
| medium object (extra keys stripped) | 1.8M | 2.
|
|
617
|
-
| medium object (invalid) |
|
|
618
|
-
| large object (10 items) | 120K |
|
|
619
|
-
| large object (100 items) | 13K |
|
|
620
|
-
| readonly field (wrapper compiles away) | 3.
|
|
621
|
-
| readonly root object (rebuild + freeze) | 2.
|
|
622
|
-
| readonly array (delegates to Zod) |
|
|
623
|
-
| recursive tree (7 nodes) |
|
|
624
|
-
| recursive tree (121 nodes) |
|
|
625
|
-
| nested recursion (7 nodes) |
|
|
626
|
-
| nested recursion (121 nodes) | 24K | 42K | **
|
|
627
|
-
| deeply nested object (243 leaves) | 11K |
|
|
628
|
-
| event log (combined) |
|
|
629
|
-
|
|
|
630
|
-
|
|
|
583
|
+
| simple string | 13.1M | 15.9M | **17.3M** | 18.0M | 17.6M | 1.1x |
|
|
584
|
+
| string (min/max) | 12.3M | 7.3M | **17.3M** | 17.2M | 15.6M | 2.4x |
|
|
585
|
+
| number (int+positive) | 12.4M | 9.7M | **17.5M** | 17.7M | 17.0M | 1.8x |
|
|
586
|
+
| enum | 11.6M | 15.5M | **17.8M** | 18.1M | 17.5M | 1.1x |
|
|
587
|
+
| bigint (min/max) | 11.9M | 8.6M | **17.3M** | — | — | 2.0x |
|
|
588
|
+
| tuple [string, int, bool] | 5.5M | 7.4M | **16.7M** | 16.0M | 15.8M | 2.3x |
|
|
589
|
+
| record\<string, number\> | 3.2M | 2.5M | **12.9M** | 12.0M | 15.2M | 5.1x |
|
|
590
|
+
| set\<string\> (5 items) | 3.6M | 2.3M | **15.3M** | — | — | 6.8x |
|
|
591
|
+
| set\<string\> (20 items) | 1.3M | 679K | **12.2M** | — | — | **18x** |
|
|
592
|
+
| map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.5M** | — | — | **10x** |
|
|
593
|
+
| map\<string, number\> (20 entries) | 628K | 346K | **8.5M** | — | — | **25x** |
|
|
594
|
+
| pipe (non-transform) | 8.8M | 4.7M | **16.9M** | — | — | 3.6x |
|
|
595
|
+
| discriminatedUnion (3 variants) | 3.4M | 5.2M | **17.1M** | 16.5M | 7.9M | 3.3x |
|
|
596
|
+
| discriminatedUnion (8 variants, rotating) | 2.6M | 4.6M | **10.5M** | — | — | 2.3x |
|
|
597
|
+
| plain union of 8 tagged objects (auto-discrim.) | 357K | 1.3M | **10.5M** | — | — | 7.9x |
|
|
598
|
+
| strict object (DB row) | 1.8M | 2.9M | **11.4M** | — | — | 3.8x |
|
|
599
|
+
| medium object (valid) | 1.9M | 2.3M | **9.8M** | 11.1M | 7.6M | 4.2x |
|
|
600
|
+
| medium object (extra keys stripped) | 1.8M | 2.1M | **9.7M** | — | — | 4.6x |
|
|
601
|
+
| medium object (invalid) | 517K | 364K | **15.9M** | 2.8M | 7.5M | **44x** |
|
|
602
|
+
| large object (10 items) | 120K | 175K | **5.3M** | 6.0M | 1.2M | **30x** |
|
|
603
|
+
| large object (100 items) | 13K | 18K | **764K** | 1.3M | 124K | **43x** |
|
|
604
|
+
| readonly field (wrapper compiles away) | 3.1M | 6.6M | **17.8M** | — | — | 2.7x |
|
|
605
|
+
| readonly root object (rebuild + freeze) | 2.9M | 5.6M | **13.5M** | — | — | 2.4x |
|
|
606
|
+
| readonly array (delegates to Zod) | 4.0M | 4.4M | **4.4M** | — | — | 1.0x |
|
|
607
|
+
| recursive tree (7 nodes) | 574K | 997K | **9.0M** | 11.9M | 4.8M | 9.0x |
|
|
608
|
+
| recursive tree (121 nodes) | 32K | 58K | **986K** | 1.9M | 375K | **17x** |
|
|
609
|
+
| nested recursion (7 nodes) | 390K | 696K | **7.8M** | 10.5M | 2.9M | **11x** |
|
|
610
|
+
| nested recursion (121 nodes) | 24K | 42K | **816K** | 1.6M | 205K | **20x** |
|
|
611
|
+
| deeply nested object (243 leaves) | 11K | 28K | **845K** | 1.0M | 125K | **30x** |
|
|
612
|
+
| event log (combined) | 375K | 808K | **7.6M** | — | — | 9.4x |
|
|
613
|
+
| catalog product with image URLs (valid) | 432K | 467K | **2.1M** | — | — | 4.6x |
|
|
614
|
+
| catalog product with image URLs (invalid) | 114K | 106K | **14.8M** | — | — | **140x** |
|
|
615
|
+
| catalog page (20 products) | 21K | 23K | **122K** | — | — | 5.2x |
|
|
616
|
+
| object with transform (zero-capture) | 1.1M | 1.9M | **7.6M** | — | — | 4.0x |
|
|
617
|
+
| array 10 × transform (zero-capture) | 123K | 209K | **4.3M** | — | — | **21x** |
|
|
631
618
|
| array 50 × transform (zero-capture) | 26K | 41K | **1.0M** | — | — | **25x** |
|
|
632
|
-
| object with captured transform | 1.2M |
|
|
633
|
-
| object with captured refine (cross-field) | 1.
|
|
634
|
-
| object with superRefine (cross-field) | 1.
|
|
635
|
-
| coerced query object (valid) | 1.8M |
|
|
636
|
-
| coerced query object (invalid) | 1.0M |
|
|
637
|
-
| preprocessed query object (valid) |
|
|
638
|
-
| preprocessed query object (invalid) |
|
|
619
|
+
| object with captured transform | 1.2M | 8.3M | **16.2M** | — | — | 2.0x |
|
|
620
|
+
| object with captured refine (cross-field) | 1.5M | 2.2M | **11.7M** | — | — | 5.3x |
|
|
621
|
+
| object with superRefine (cross-field) | 1.5M | 2.2M | **9.6M** | — | — | 4.3x |
|
|
622
|
+
| coerced query object (valid) | 1.8M | 3.0M | **5.4M** | — | — | 1.8x |
|
|
623
|
+
| coerced query object (invalid) | 1.0M | 837K | **10.6M** | — | — | **13x** |
|
|
624
|
+
| preprocessed query object (valid) | 432K | 1.8M | **5.4M** | — | — | 3.0x |
|
|
625
|
+
| preprocessed query object (invalid) | 393K | 796K | **12.7M** | — | — | **16x** |
|
|
639
626
|
| stringbool config object (valid) | — | 2.8M | **6.3M** | — | — | 2.2x |
|
|
640
|
-
| stringbool config object (invalid) | — |
|
|
641
|
-
| custom/instanceof request (valid) |
|
|
642
|
-
| custom/instanceof request (invalid) |
|
|
643
|
-
| disjoint object intersection (valid) | 1.4M | 1.6M | **9.
|
|
644
|
-
| disjoint object intersection (invalid) |
|
|
627
|
+
| stringbool config object (invalid) | — | 698K | **13.5M** | — | — | **19x** |
|
|
628
|
+
| custom/instanceof request (valid) | 979K | 3.2M | **10.4M** | — | — | 3.3x |
|
|
629
|
+
| custom/instanceof request (invalid) | 795K | 955K | **13.6M** | — | — | **14x** |
|
|
630
|
+
| disjoint object intersection (valid) | 1.4M | 1.6M | **9.7M** | — | — | 6.0x |
|
|
631
|
+
| disjoint object intersection (invalid) | 494K | 335K | **15.7M** | — | — | **47x** |
|
|
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`, `z.url()`, defaults,
|
|
650
|
+
transforms, context-free preprocessors, disjoint-key intersections) instead validate and rebuild in a
|
|
651
|
+
single pass 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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBAwMgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBA+EtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
|
|
@@ -1,11 +1,11 @@
|
|
|
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";
|
|
6
6
|
import { parsedProperties } from "./schemas/object.js";
|
|
7
7
|
import { innerAppliesDefaultOnUndefined } from "./schemas/optional.js";
|
|
8
|
-
import { fastStringCheck } from "./schemas/string.js";
|
|
8
|
+
import { buildUrlCheck, fastStringCheck, isUrlRewrite } from "./schemas/string.js";
|
|
9
9
|
import { emitStringBoolMap, stringBoolInlineHit, stringBoolUsesInline } from "./schemas/string-bool.js";
|
|
10
10
|
import { createFastGen, generateFast } from "./fast-path.js";
|
|
11
11
|
//#region src/core/codegen/build-path.ts
|
|
@@ -40,7 +40,7 @@ function rebuildSet(root, includeProtoScrub = true) {
|
|
|
40
40
|
for (const node of nodes) {
|
|
41
41
|
if (rebuilds.has(node)) continue;
|
|
42
42
|
const target = node.type === "recursiveRef" ? targets.get(node.refId ?? 0) : void 0;
|
|
43
|
-
if (node.type === "object" && node.stripUnknownKeys === true || node.type === "readonly" && node.freeze === true || node.type === "default" || node.type === "tuple" && tupleRewritesShortInput(node) || includeProtoScrub && needsProtoScrub(node) || node.type === "stringBool" || node.type === "string" && (node.coerce === true || node.checks.some((c) => c.kind === "overwrite_effect")) || (node.type === "number" || node.type === "boolean" || node.type === "bigint" || node.type === "date") && node.coerce === true || node.type === "effect" || target !== void 0 && rebuilds.has(target) || children(node).some((child) => rebuilds.has(child))) {
|
|
43
|
+
if (node.type === "object" && node.stripUnknownKeys === true || node.type === "readonly" && node.freeze === true || node.type === "default" || node.type === "tuple" && tupleRewritesShortInput(node) || includeProtoScrub && needsProtoScrub(node) || node.type === "stringBool" || node.type === "string" && (node.coerce === true || node.checks.some((c) => c.kind === "overwrite_effect" || isUrlRewrite(c))) || (node.type === "number" || node.type === "boolean" || node.type === "bigint" || node.type === "date") && node.coerce === true || node.type === "effect" || target !== void 0 && rebuilds.has(target) || children(node).some((child) => rebuilds.has(child))) {
|
|
44
44
|
rebuilds.add(node);
|
|
45
45
|
changed = true;
|
|
46
46
|
}
|
|
@@ -95,7 +95,7 @@ function fastResultIsInput(ir) {
|
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
97
|
* True when the subtree mutates for any reason the build pass cannot reproduce —
|
|
98
|
-
* `.catch()`, `
|
|
98
|
+
* `.catch()`, `superRefine`. Those rewrite values in ways this pass
|
|
99
99
|
* (which validates, coerces, decodes string booleans, substitutes declared
|
|
100
100
|
* defaults, applies ordered string rewrites and copies) does not model, so the
|
|
101
101
|
* schema keeps the eager walk.
|
|
@@ -110,7 +110,7 @@ function mutatesBeyondStrip(ir) {
|
|
|
110
110
|
*/
|
|
111
111
|
function mutatesHere(ir) {
|
|
112
112
|
switch (ir.type) {
|
|
113
|
-
case "string": return superRefines(ir.checks)
|
|
113
|
+
case "string": return superRefines(ir.checks);
|
|
114
114
|
case "number": return superRefines(ir.checks);
|
|
115
115
|
case "boolean":
|
|
116
116
|
case "bigint":
|
|
@@ -181,6 +181,9 @@ function generateBuild(ir, ctx) {
|
|
|
181
181
|
temps: [],
|
|
182
182
|
used: 0
|
|
183
183
|
};
|
|
184
|
+
const name = emitTemp(ctx, "vb");
|
|
185
|
+
const recNames = ctx.buildRecNames ??= /* @__PURE__ */ new Map();
|
|
186
|
+
recNames.set(0, name);
|
|
184
187
|
const built = build(ir, "input", {
|
|
185
188
|
ctx,
|
|
186
189
|
extractable: false,
|
|
@@ -188,8 +191,10 @@ function generateBuild(ir, ctx) {
|
|
|
188
191
|
rebuilds,
|
|
189
192
|
scope
|
|
190
193
|
});
|
|
191
|
-
if (built === null)
|
|
192
|
-
|
|
194
|
+
if (built === null) {
|
|
195
|
+
recNames.delete(0);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
193
198
|
ctx.preamble.push(`function ${name}(input){${declareFastTemps(scope)}${built.code}return ${built.value};}`);
|
|
194
199
|
return name;
|
|
195
200
|
}
|
|
@@ -731,6 +736,10 @@ function buildString(ir, input, g) {
|
|
|
731
736
|
break;
|
|
732
737
|
case "super_refine_effect": return null;
|
|
733
738
|
default: {
|
|
739
|
+
if (check.kind === "string_format" && isUrlRewrite(check)) {
|
|
740
|
+
code += buildUrlCheck(check, value, g.fail, (prefix) => local(g, prefix), g.ctx);
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
734
743
|
const expr = fastStringCheck(check, value, g.ctx);
|
|
735
744
|
if (expr === null) return null;
|
|
736
745
|
code += `if(!(${expr}))return ${g.fail};`;
|