zod-compiler 2.0.1 → 2.0.3
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 +62 -58
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +5 -0
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/core/codegen/build-path.d.ts.map +1 -1
- package/dist/core/codegen/build-path.js +107 -28
- package/dist/core/codegen/build-path.js.map +1 -1
- package/dist/core/codegen/context.d.ts +21 -11
- package/dist/core/codegen/context.d.ts.map +1 -1
- package/dist/core/codegen/context.js +15 -1
- package/dist/core/codegen/context.js.map +1 -1
- package/dist/core/codegen/fast-path.d.ts +3 -1
- package/dist/core/codegen/fast-path.d.ts.map +1 -1
- package/dist/core/codegen/fast-path.js +7 -4
- package/dist/core/codegen/fast-path.js.map +1 -1
- package/dist/core/codegen/index.js +4 -3
- package/dist/core/codegen/index.js.map +1 -1
- package/dist/core/codegen/issue-decls.d.ts +67 -5
- package/dist/core/codegen/issue-decls.d.ts.map +1 -1
- package/dist/core/codegen/issue-decls.js +72 -7
- 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 +3 -3
- package/dist/core/codegen/schemas/array.js.map +1 -1
- package/dist/core/codegen/schemas/default.d.ts +10 -0
- package/dist/core/codegen/schemas/default.d.ts.map +1 -1
- package/dist/core/codegen/schemas/default.js +11 -0
- package/dist/core/codegen/schemas/default.js.map +1 -1
- package/dist/core/codegen/schemas/effect.d.ts +13 -1
- package/dist/core/codegen/schemas/effect.d.ts.map +1 -1
- package/dist/core/codegen/schemas/effect.js +26 -3
- package/dist/core/codegen/schemas/effect.js.map +1 -1
- package/dist/core/codegen/schemas/fallback.d.ts +17 -1
- package/dist/core/codegen/schemas/fallback.d.ts.map +1 -1
- package/dist/core/codegen/schemas/fallback.js +31 -15
- package/dist/core/codegen/schemas/fallback.js.map +1 -1
- package/dist/core/codegen/schemas/number.d.ts.map +1 -1
- package/dist/core/codegen/schemas/number.js +35 -35
- package/dist/core/codegen/schemas/number.js.map +1 -1
- package/dist/core/codegen/schemas/object.d.ts.map +1 -1
- package/dist/core/codegen/schemas/object.js +64 -29
- package/dist/core/codegen/schemas/object.js.map +1 -1
- package/dist/core/codegen/schemas/string.d.ts.map +1 -1
- package/dist/core/codegen/schemas/string.js +3 -3
- package/dist/core/codegen/schemas/string.js.map +1 -1
- package/dist/core/codegen/schemas/template-literal.d.ts.map +1 -1
- package/dist/core/codegen/schemas/template-literal.js +1 -1
- package/dist/core/codegen/schemas/template-literal.js.map +1 -1
- package/dist/core/extract/index.d.ts.map +1 -1
- package/dist/core/extract/index.js +2 -0
- package/dist/core/extract/index.js.map +1 -1
- package/dist/core/extract/zod-version.d.ts +78 -0
- package/dist/core/extract/zod-version.d.ts.map +1 -0
- package/dist/core/extract/zod-version.js +109 -0
- package/dist/core/extract/zod-version.js.map +1 -0
- package/dist/core/iife.d.ts +8 -0
- package/dist/core/iife.d.ts.map +1 -1
- package/dist/core/iife.js +8 -0
- package/dist/core/iife.js.map +1 -1
- package/dist/jit.d.ts.map +1 -1
- package/dist/jit.js +9 -1
- package/dist/jit.js.map +1 -1
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +4 -1
- package/dist/unplugin/hoist-compile.d.ts.map +1 -1
- package/dist/unplugin/hoist-compile.js +3 -1
- package/dist/unplugin/hoist-compile.js.map +1 -1
- package/dist/unplugin/transform.d.ts.map +1 -1
- package/dist/unplugin/transform.js +5 -0
- package/dist/unplugin/transform.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
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 44x faster** validation, and up to **46x** on rejected
|
|
6
6
|
input. No code changes required.
|
|
7
7
|
|
|
8
8
|
Requires **Zod ≥ 4.5**. Compiled output reproduces 4.5's semantics exactly, down to code-point string
|
|
9
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.
|
|
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.
|
|
11
13
|
|
|
12
14
|
- [What Gets Compiled](#what-gets-compiled)
|
|
13
15
|
- [Schema Hoisting](#schema-hoisting)
|
|
@@ -26,7 +28,7 @@ loads pre-generated validators.
|
|
|
26
28
|
| | zod-compiler (build plugins / CLI) | Zod `z.compile()` |
|
|
27
29
|
| -------------------------------------- | -------------------------------------------- | --------------------------------------------- |
|
|
28
30
|
| Compilation | Build time (true AOT) | Runtime (`z.compile()` or the first parse) |
|
|
29
|
-
| Reported validation speedup | Up to
|
|
31
|
+
| Reported validation speedup | Up to 44x; up to 46x on rejected input | ~9x in Zod's headline example |
|
|
30
32
|
| Uses `new Function()` at runtime\* | No | Yes |
|
|
31
33
|
| Cold start | Fast; the validator is already generated | Pays for code generation at startup/first use |
|
|
32
34
|
| Strict CSP without `'unsafe-eval'` | Supported | Compilation is unavailable |
|
|
@@ -543,7 +545,7 @@ npx zod-compiler check src/schemas.ts --json --fail-under 80
|
|
|
543
545
|
|
|
544
546
|
## What Gets Compiled
|
|
545
547
|
|
|
546
|
-
### Fully Compiled (up to
|
|
548
|
+
### Fully Compiled (up to 44x faster)
|
|
547
549
|
|
|
548
550
|
Every Zod type except the fallbacks below: all primitives, `object` / `strictObject` / `looseObject`,
|
|
549
551
|
`array`, `tuple`, `record`, `set`, `map`, `union`, `discriminatedUnion`, `intersection`, `pipe`,
|
|
@@ -577,11 +579,11 @@ not the whole object. Run `zod-compiler check` to see what compiled.
|
|
|
577
579
|
Compiled validators match Zod on verdicts, output data and error messages, including issue ordering.
|
|
578
580
|
Three things differ by design:
|
|
579
581
|
|
|
580
|
-
| Behavior | Zod | zod-compiler
|
|
581
|
-
| ------------------------- | ----------------------------------------------- |
|
|
582
|
-
| Record key iteration | Own enumerable keys, symbols included | Own enumerable **string** keys only
|
|
583
|
-
| Container output identity | A fresh array / set / map / object | The input container, by reference (array holes survive) |
|
|
584
|
-
| Per-call parse params | `safeParse(x, { error, reportInput })` honoured | Ignored; global `z.config()` maps still apply
|
|
582
|
+
| Behavior | Zod | zod-compiler |
|
|
583
|
+
| ------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
584
|
+
| Record key iteration | Own enumerable keys, symbols included | Own enumerable **string** keys only |
|
|
585
|
+
| Container output identity | A fresh array / set / map / object | The input container, by reference (array holes and the input's key order survive); a rebuilt object is fresh and in zod's key order |
|
|
586
|
+
| Per-call parse params | `safeParse(x, { error, reportInput })` honoured | Ignored; global `z.config()` maps still apply |
|
|
585
587
|
|
|
586
588
|
Schema-level `error` and `z.config()` maps are unaffected; for a per-call map use
|
|
587
589
|
`z.safeParse(Schema, x, params)`.
|
|
@@ -594,52 +596,52 @@ Schema-level `error` and `z.config()` maps are unaffected; for a per-call map us
|
|
|
594
596
|
|
|
595
597
|
| Scenario | Zod v3 | Zod v4 | **zod-compiler** | Typia | AJV | vs Zod v4 |
|
|
596
598
|
| ----------------------------------------------- | ------ | ------ | ---------------- | ----- | ----- | --------- |
|
|
597
|
-
| simple string | 12.
|
|
598
|
-
| string (min/max) |
|
|
599
|
-
| number (int+positive) |
|
|
600
|
-
| enum |
|
|
601
|
-
| bigint (min/max) | 11.
|
|
602
|
-
| tuple [string, int, bool] | 5.
|
|
603
|
-
| record\<string, number\> | 3.1M | 2.
|
|
604
|
-
| set\<string\> (5 items) | 3.
|
|
605
|
-
| set\<string\> (20 items) | 1.3M |
|
|
606
|
-
| map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.
|
|
607
|
-
| map\<string, number\> (20 entries) |
|
|
608
|
-
| pipe (non-transform) | 8.
|
|
609
|
-
| discriminatedUnion (3 variants) | 3.3M | 5.2M | **
|
|
610
|
-
| discriminatedUnion (8 variants, rotating) | 2.6M | 4.4M | **
|
|
611
|
-
| plain union of 8 tagged objects (auto-discrim.) |
|
|
612
|
-
| strict object (DB row) | 1.
|
|
613
|
-
| medium object (valid) | 1.9M | 2.
|
|
614
|
-
| medium object (extra keys stripped) | 1.8M | 2.
|
|
615
|
-
| medium object (invalid) |
|
|
616
|
-
| large object (10 items) |
|
|
617
|
-
| large object (100 items) | 13K |
|
|
618
|
-
| readonly field (wrapper compiles away) | 3.0M | 6.6M | **
|
|
619
|
-
| readonly root object (rebuild + freeze) | 2.8M | 5.
|
|
620
|
-
| readonly array (delegates to Zod) | 3.
|
|
621
|
-
| recursive tree (7 nodes) |
|
|
622
|
-
| recursive tree (121 nodes) | 31K |
|
|
623
|
-
| nested recursion (7 nodes) |
|
|
624
|
-
| nested recursion (121 nodes) |
|
|
625
|
-
| deeply nested object (243 leaves) | 11K | 27K | **
|
|
626
|
-
| event log (combined) |
|
|
627
|
-
| object with transform (zero-capture) | 1.1M |
|
|
628
|
-
| array 10 × transform (zero-capture) |
|
|
629
|
-
| array 50 × transform (zero-capture) |
|
|
630
|
-
| object with captured transform | 1.2M |
|
|
631
|
-
| object with captured refine (cross-field) | 1.4M | 2.2M | **11.
|
|
632
|
-
| object with superRefine (cross-field) | 1.4M | 2.
|
|
633
|
-
| coerced query object (valid) | 1.8M | 2.9M | **5.
|
|
634
|
-
| coerced query object (invalid) | 1.0M |
|
|
635
|
-
| preprocessed query object (valid) |
|
|
636
|
-
| preprocessed query object (invalid) |
|
|
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 |
|
|
622
|
+
| 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** |
|
|
637
639
|
| stringbool config object (valid) | — | 2.8M | **6.3M** | — | — | 2.2x |
|
|
638
|
-
| stringbool config object (invalid) | — |
|
|
639
|
-
| custom/instanceof request (valid) | 965K | 3.1M | **
|
|
640
|
-
| custom/instanceof request (invalid) |
|
|
641
|
-
| disjoint object intersection (valid) | 1.4M | 1.6M | **9.
|
|
642
|
-
| disjoint object intersection (invalid) |
|
|
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** |
|
|
643
645
|
|
|
644
646
|
_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),
|
|
645
647
|
best of three runs. The harness costs ~60 ns per iteration, so the fastest rows sit at that floor and gaps
|
|
@@ -662,10 +664,12 @@ failure, and is deferred until `.error` is read. A `z.object()` strips, so it in
|
|
|
662
664
|
single pass that validates and rebuilds together and bails on the first failure, covering the reshaping
|
|
663
665
|
idioms too (array size checks, `.refine()`, `.default()`, `.trim()`, `.transform()`).
|
|
664
666
|
|
|
665
|
-
Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
|
669
673
|
plain-object guard exits on one comparison for an ordinary object, and a case-insensitive `stringbool`
|
|
670
674
|
looks its input up verbatim before paying for `toLowerCase()`. Stripping objects, native coercions,
|
|
671
675
|
`stringbool`, defaults, string rewrites, context-free preprocessors and synchronous transforms validate
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","names":[],"sources":["../../../src/cli/commands/check.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"check.d.ts","names":[],"sources":["../../../src/cli/commands/check.ts"],"mappings":";UAmFiB;EACf;EACA;EACA;EACA;EACA;;iBAGoB,SAAS,SAAS,eAAe"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { discoverSchemas } from "../../discovery.js";
|
|
2
|
+
import { isUnsupportedZodVersionError } from "../../core/extract/zod-version.js";
|
|
2
3
|
import { extractSchema } from "../../core/extract/index.js";
|
|
3
4
|
import { getErrorMessage } from "../errors.js";
|
|
4
5
|
import { createColors, logger } from "../logger.js";
|
|
@@ -86,6 +87,10 @@ async function runCheck(options) {
|
|
|
86
87
|
try {
|
|
87
88
|
ir = extractSchema(s.schema, []);
|
|
88
89
|
} catch (e) {
|
|
90
|
+
if (isUnsupportedZodVersionError(e)) {
|
|
91
|
+
logger.error(getErrorMessage(e));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
89
94
|
err(`${c.red}error${c.reset} Failed to extract "${s.exportName}" in ${relPath}: ${getErrorMessage(e)}`);
|
|
90
95
|
continue;
|
|
91
96
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","names":[],"sources":["../../../src/cli/commands/check.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport type { DiagnosticNode, DiagnosticResult } from \"../../core/diagnostic.js\";\nimport { diagnoseSchema } from \"../../core/diagnostic.js\";\nimport type { RefEntry } from \"../../core/extract/index.js\";\nimport { extractSchema } from \"../../core/extract/index.js\";\nimport type { DiscoveredSchema, SchemaIR } from \"../../core/types.js\";\nimport { discoverSchemas } from \"../../discovery.js\";\nimport { getErrorMessage } from \"../errors.js\";\nimport type { Colors } from \"../logger.js\";\nimport { createColors, logger } from \"../logger.js\";\n\n// ─── Tree Rendering ─────────────────────────────────────────────────────────\n\nfunction renderTree(node: DiagnosticNode, c: Colors, prefix = \"\", isLast = true): string[] {\n const lines: string[] = [];\n const connector = prefix === \"\" ? \"\" : isLast ? \"└─ \" : \"├─ \";\n const childPrefix = prefix === \"\" ? \"\" : prefix + (isLast ? \" \" : \"│ \");\n\n const statusIcon = node.status === \"compiled\" ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;\n const typeLabel = `${c.bold}${node.type}${c.reset}`;\n const pathLabel = node.path && node.path !== \"(root)\" ? ` ${c.dim}${node.path}${c.reset}` : \"\";\n const reasonLabel =\n node.status === \"fallback\" && node.reason ? ` ${c.yellow}(${node.reason})${c.reset}` : \"\";\n\n lines.push(`${prefix}${connector}${statusIcon} ${typeLabel}${pathLabel}${reasonLabel}`);\n\n if (node.status === \"fallback\" && node.hint) {\n const hintPrefix = `${childPrefix} `;\n lines.push(`${hintPrefix}${c.dim}hint: ${node.hint}${c.reset}`);\n }\n\n for (const [i, child] of node.children.entries()) {\n const last = i === node.children.length - 1;\n lines.push(...renderTree(child, c, childPrefix, last));\n }\n\n return lines;\n}\n\n// ─── JSON Output ────────────────────────────────────────────────────────────\n\ninterface JsonSchemaReport {\n exportName: string;\n coverage: { total: number; compilable: number; percent: number };\n fastPath: { eligible: boolean; blocker?: string };\n fallbacks: { reason: string; path: string; hint: string }[];\n}\n\ninterface JsonReport {\n file: string;\n schemas: JsonSchemaReport[];\n}\n\nfunction buildJsonReport(\n filePath: string,\n results: { exportName: string; diagnostic: DiagnosticResult }[],\n): JsonReport {\n return {\n file: filePath,\n schemas: results.map(({ exportName, diagnostic }) => {\n const report: JsonSchemaReport = {\n exportName,\n coverage: {\n total: diagnostic.total,\n compilable: diagnostic.compilable,\n percent: diagnostic.coveragePct,\n },\n fastPath: { eligible: diagnostic.fastPathEligible },\n fallbacks: diagnostic.fallbacks,\n };\n if (diagnostic.fastPathBlocker) {\n report.fastPath.blocker = diagnostic.fastPathBlocker;\n }\n return report;\n }),\n };\n}\n\n// ─── Command ────────────────────────────────────────────────────────────────\n\nexport interface CheckOptions {\n inputs: string[];\n json: boolean;\n failUnder: number | undefined;\n noColor: boolean;\n autoDiscover: boolean;\n}\n\nexport async function runCheck(options: CheckOptions): Promise<void> {\n // oxlint-disable-next-line no-console -- CLI output\n const out = console.log;\n // oxlint-disable-next-line no-console -- CLI output\n const err = console.error;\n const colorEnabled = !options.noColor && process.stdout.isTTY === true;\n const c = createColors(colorEnabled);\n const files: string[] = [];\n\n for (const input of options.inputs) {\n const absPath = path.resolve(input);\n const stat = await fs.promises.stat(absPath).catch(() => null);\n\n if (!stat) {\n logger.error(`File not found: ${input}`);\n process.exit(1);\n }\n\n if (stat.isDirectory()) {\n logger.error(\"check command expects file paths, not directories.\");\n process.exit(1);\n }\n\n files.push(absPath);\n }\n\n const allJsonReports: JsonReport[] = [];\n let minCoverage = 100;\n let schemasProcessed = 0;\n\n for (const filePath of files) {\n const relPath = path.relative(process.cwd(), filePath);\n\n let schemas: DiscoveredSchema[];\n try {\n schemas = await discoverSchemas(filePath, { autoDiscover: options.autoDiscover });\n } catch (e) {\n logger.error(`Failed to load ${relPath}: ${getErrorMessage(e)}`);\n process.exit(1);\n }\n\n if (schemas.length === 0) {\n const hint = options.autoDiscover\n ? \"no Zod schemas exported\"\n : \"no compile() calls found (use --schemas auto to scan plain Zod exports)\";\n logger.warn(`${relPath}: ${hint}`);\n continue;\n }\n\n const results: { exportName: string; diagnostic: DiagnosticResult }[] = [];\n\n for (const s of schemas) {\n let ir: SchemaIR;\n try {\n // Collect refs exactly as the compile pipeline does. A callback that\n // captures is compiled by REFERENCE through an `__rf[]` entry, which\n // only happens when a ref table is present — extracting without one\n // reports every captured refine/transform and every superRefine as a\n // fallback, so `check` claimed 0% on schemas `generate` compiles in\n // full and told the user to rewrite code that was already fast.\n const refEntries: RefEntry[] = [];\n ir = extractSchema(s.schema, refEntries);\n } catch (e) {\n err(\n `${c.red}error${c.reset} Failed to extract \"${s.exportName}\" in ${relPath}: ${getErrorMessage(e)}`,\n );\n continue;\n }\n const diagnostic = diagnoseSchema(ir);\n results.push({ exportName: s.exportName, diagnostic });\n schemasProcessed++;\n\n if (diagnostic.coveragePct < minCoverage) {\n minCoverage = diagnostic.coveragePct;\n }\n }\n\n if (options.json) {\n allJsonReports.push(buildJsonReport(relPath, results));\n } else {\n out(`\\n${c.bold}${relPath}${c.reset}`);\n\n for (const { exportName, diagnostic } of results) {\n const pctColor = diagnostic.coveragePct === 100 ? c.green : c.yellow;\n const fpLabel = diagnostic.fastPathEligible\n ? `${c.green}Fast Path: eligible${c.reset}`\n : `${c.yellow}Fast Path: ineligible${c.reset}${c.dim} (${diagnostic.fastPathBlocker})${c.reset}`;\n\n out(\n `\\n ${c.cyan}${exportName}${c.reset} — ${pctColor}${diagnostic.coveragePct}%${c.reset} compiled (${diagnostic.compilable}/${diagnostic.total} nodes) | ${fpLabel}`,\n );\n\n const treeLines = renderTree(diagnostic.root, c, \" \");\n for (const line of treeLines) {\n out(line);\n }\n\n if (diagnostic.fallbacks.length > 0) {\n out(`\\n ${c.yellow}Fallbacks:${c.reset}`);\n for (const fb of diagnostic.fallbacks) {\n out(` ${c.red}✗${c.reset} ${fb.path} — ${fb.reason}`);\n out(` ${c.dim}${fb.hint}${c.reset}`);\n }\n }\n }\n }\n }\n\n if (options.json) {\n out(JSON.stringify(allJsonReports, null, 2));\n }\n\n if (options.failUnder !== undefined) {\n if (schemasProcessed === 0) {\n logger.error(\"No schemas were analyzed — cannot satisfy --fail-under\");\n process.exit(1);\n }\n if (minCoverage < options.failUnder) {\n logger.error(`Coverage ${minCoverage}% is below threshold ${options.failUnder}%`);\n process.exit(1);\n }\n } else if (schemasProcessed === 0) {\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;AAeA,SAAS,WAAW,MAAsB,GAAW,SAAS,IAAI,SAAS,MAAgB;CACzF,MAAM,QAAkB,CAAC;CACzB,MAAM,YAAY,WAAW,KAAK,KAAK,SAAS,QAAQ;CACxD,MAAM,cAAc,WAAW,KAAK,KAAK,UAAU,SAAS,QAAQ;CAEpE,MAAM,aAAa,KAAK,WAAW,aAAa,GAAG,EAAE,MAAM,GAAG,EAAE,UAAU,GAAG,EAAE,IAAI,GAAG,EAAE;CACxF,MAAM,YAAY,GAAG,EAAE,OAAO,KAAK,OAAO,EAAE;CAC5C,MAAM,YAAY,KAAK,QAAQ,KAAK,SAAS,WAAW,IAAI,EAAE,MAAM,KAAK,OAAO,EAAE,UAAU;CAC5F,MAAM,cACJ,KAAK,WAAW,cAAc,KAAK,SAAS,IAAI,EAAE,OAAO,GAAG,KAAK,OAAO,GAAG,EAAE,UAAU;CAEzF,MAAM,KAAK,GAAG,SAAS,YAAY,WAAW,GAAG,YAAY,YAAY,aAAa;CAEtF,IAAI,KAAK,WAAW,cAAc,KAAK,MAAM;EAC3C,MAAM,aAAa,GAAG,YAAY;EAClC,MAAM,KAAK,GAAG,aAAa,EAAE,IAAI,QAAQ,KAAK,OAAO,EAAE,OAAO;CAChE;CAEA,KAAK,MAAM,CAAC,GAAG,UAAU,KAAK,SAAS,QAAQ,GAAG;EAChD,MAAM,OAAO,MAAM,KAAK,SAAS,SAAS;EAC1C,MAAM,KAAK,GAAG,WAAW,OAAO,GAAG,aAAa,IAAI,CAAC;CACvD;CAEA,OAAO;AACT;AAgBA,SAAS,gBACP,UACA,SACY;CACZ,OAAO;EACL,MAAM;EACN,SAAS,QAAQ,KAAK,EAAE,YAAY,iBAAiB;GACnD,MAAM,SAA2B;IAC/B;IACA,UAAU;KACR,OAAO,WAAW;KAClB,YAAY,WAAW;KACvB,SAAS,WAAW;IACtB;IACA,UAAU,EAAE,UAAU,WAAW,iBAAiB;IAClD,WAAW,WAAW;GACxB;GACA,IAAI,WAAW,iBACb,OAAO,SAAS,UAAU,WAAW;GAEvC,OAAO;EACT,CAAC;CACH;AACF;AAYA,eAAsB,SAAS,SAAsC;CAEnE,MAAM,MAAM,QAAQ;CAEpB,MAAM,MAAM,QAAQ;CAEpB,MAAM,IAAI,aADW,CAAC,QAAQ,WAAW,QAAQ,OAAO,UAAU,IAC/B;CACnC,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,QAAQ,QAAQ;EAClC,MAAM,UAAU,KAAK,QAAQ,KAAK;EAClC,MAAM,OAAO,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,CAAC,YAAY,IAAI;EAE7D,IAAI,CAAC,MAAM;GACT,OAAO,MAAM,mBAAmB,OAAO;GACvC,QAAQ,KAAK,CAAC;EAChB;EAEA,IAAI,KAAK,YAAY,GAAG;GACtB,OAAO,MAAM,oDAAoD;GACjE,QAAQ,KAAK,CAAC;EAChB;EAEA,MAAM,KAAK,OAAO;CACpB;CAEA,MAAM,iBAA+B,CAAC;CACtC,IAAI,cAAc;CAClB,IAAI,mBAAmB;CAEvB,KAAK,MAAM,YAAY,OAAO;EAC5B,MAAM,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,QAAQ;EAErD,IAAI;EACJ,IAAI;GACF,UAAU,MAAM,gBAAgB,UAAU,EAAE,cAAc,QAAQ,aAAa,CAAC;EAClF,SAAS,GAAG;GACV,OAAO,MAAM,kBAAkB,QAAQ,IAAI,gBAAgB,CAAC,GAAG;GAC/D,QAAQ,KAAK,CAAC;EAChB;EAEA,IAAI,QAAQ,WAAW,GAAG;GACxB,MAAM,OAAO,QAAQ,eACjB,4BACA;GACJ,OAAO,KAAK,GAAG,QAAQ,IAAI,MAAM;GACjC;EACF;EAEA,MAAM,UAAkE,CAAC;EAEzE,KAAK,MAAM,KAAK,SAAS;GACvB,IAAI;GACJ,IAAI;IAQF,KAAK,cAAc,EAAE,QAAQ,CAAS,CAAC;GACzC,SAAS,GAAG;IACV,IACE,GAAG,EAAE,IAAI,OAAO,EAAE,MAAM,sBAAsB,EAAE,WAAW,OAAO,QAAQ,IAAI,gBAAgB,CAAC,GACjG;IACA;GACF;GACA,MAAM,aAAa,eAAe,EAAE;GACpC,QAAQ,KAAK;IAAE,YAAY,EAAE;IAAY;GAAW,CAAC;GACrD;GAEA,IAAI,WAAW,cAAc,aAC3B,cAAc,WAAW;EAE7B;EAEA,IAAI,QAAQ,MACV,eAAe,KAAK,gBAAgB,SAAS,OAAO,CAAC;OAChD;GACL,IAAI,KAAK,EAAE,OAAO,UAAU,EAAE,OAAO;GAErC,KAAK,MAAM,EAAE,YAAY,gBAAgB,SAAS;IAChD,MAAM,WAAW,WAAW,gBAAgB,MAAM,EAAE,QAAQ,EAAE;IAC9D,MAAM,UAAU,WAAW,mBACvB,GAAG,EAAE,MAAM,qBAAqB,EAAE,UAClC,GAAG,EAAE,OAAO,uBAAuB,EAAE,QAAQ,EAAE,IAAI,IAAI,WAAW,gBAAgB,GAAG,EAAE;IAE3F,IACE,OAAO,EAAE,OAAO,aAAa,EAAE,MAAM,KAAK,WAAW,WAAW,YAAY,GAAG,EAAE,MAAM,aAAa,WAAW,WAAW,GAAG,WAAW,MAAM,YAAY,SAC5J;IAEA,MAAM,YAAY,WAAW,WAAW,MAAM,GAAG,MAAM;IACvD,KAAK,MAAM,QAAQ,WACjB,IAAI,IAAI;IAGV,IAAI,WAAW,UAAU,SAAS,GAAG;KACnC,IAAI,SAAS,EAAE,OAAO,YAAY,EAAE,OAAO;KAC3C,KAAK,MAAM,MAAM,WAAW,WAAW;MACrC,IAAI,SAAS,EAAE,IAAI,GAAG,EAAE,MAAM,GAAG,GAAG,KAAK,KAAK,GAAG,QAAQ;MACzD,IAAI,WAAW,EAAE,MAAM,GAAG,OAAO,EAAE,OAAO;KAC5C;IACF;GACF;EACF;CACF;CAEA,IAAI,QAAQ,MACV,IAAI,KAAK,UAAU,gBAAgB,MAAM,CAAC,CAAC;CAG7C,IAAI,QAAQ,cAAc,KAAA,GAAW;EACnC,IAAI,qBAAqB,GAAG;GAC1B,OAAO,MAAM,wDAAwD;GACrE,QAAQ,KAAK,CAAC;EAChB;EACA,IAAI,cAAc,QAAQ,WAAW;GACnC,OAAO,MAAM,YAAY,YAAY,uBAAuB,QAAQ,UAAU,EAAE;GAChF,QAAQ,KAAK,CAAC;EAChB;CACF,OAAO,IAAI,qBAAqB,GAC9B,QAAQ,KAAK,CAAC;AAElB"}
|
|
1
|
+
{"version":3,"file":"check.js","names":[],"sources":["../../../src/cli/commands/check.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport type { DiagnosticNode, DiagnosticResult } from \"../../core/diagnostic.js\";\nimport { diagnoseSchema } from \"../../core/diagnostic.js\";\nimport type { RefEntry } from \"../../core/extract/index.js\";\nimport { extractSchema } from \"../../core/extract/index.js\";\nimport { isUnsupportedZodVersionError } from \"../../core/extract/zod-version.js\";\nimport type { DiscoveredSchema, SchemaIR } from \"../../core/types.js\";\nimport { discoverSchemas } from \"../../discovery.js\";\nimport { getErrorMessage } from \"../errors.js\";\nimport type { Colors } from \"../logger.js\";\nimport { createColors, logger } from \"../logger.js\";\n\n// ─── Tree Rendering ─────────────────────────────────────────────────────────\n\nfunction renderTree(node: DiagnosticNode, c: Colors, prefix = \"\", isLast = true): string[] {\n const lines: string[] = [];\n const connector = prefix === \"\" ? \"\" : isLast ? \"└─ \" : \"├─ \";\n const childPrefix = prefix === \"\" ? \"\" : prefix + (isLast ? \" \" : \"│ \");\n\n const statusIcon = node.status === \"compiled\" ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;\n const typeLabel = `${c.bold}${node.type}${c.reset}`;\n const pathLabel = node.path && node.path !== \"(root)\" ? ` ${c.dim}${node.path}${c.reset}` : \"\";\n const reasonLabel =\n node.status === \"fallback\" && node.reason ? ` ${c.yellow}(${node.reason})${c.reset}` : \"\";\n\n lines.push(`${prefix}${connector}${statusIcon} ${typeLabel}${pathLabel}${reasonLabel}`);\n\n if (node.status === \"fallback\" && node.hint) {\n const hintPrefix = `${childPrefix} `;\n lines.push(`${hintPrefix}${c.dim}hint: ${node.hint}${c.reset}`);\n }\n\n for (const [i, child] of node.children.entries()) {\n const last = i === node.children.length - 1;\n lines.push(...renderTree(child, c, childPrefix, last));\n }\n\n return lines;\n}\n\n// ─── JSON Output ────────────────────────────────────────────────────────────\n\ninterface JsonSchemaReport {\n exportName: string;\n coverage: { total: number; compilable: number; percent: number };\n fastPath: { eligible: boolean; blocker?: string };\n fallbacks: { reason: string; path: string; hint: string }[];\n}\n\ninterface JsonReport {\n file: string;\n schemas: JsonSchemaReport[];\n}\n\nfunction buildJsonReport(\n filePath: string,\n results: { exportName: string; diagnostic: DiagnosticResult }[],\n): JsonReport {\n return {\n file: filePath,\n schemas: results.map(({ exportName, diagnostic }) => {\n const report: JsonSchemaReport = {\n exportName,\n coverage: {\n total: diagnostic.total,\n compilable: diagnostic.compilable,\n percent: diagnostic.coveragePct,\n },\n fastPath: { eligible: diagnostic.fastPathEligible },\n fallbacks: diagnostic.fallbacks,\n };\n if (diagnostic.fastPathBlocker) {\n report.fastPath.blocker = diagnostic.fastPathBlocker;\n }\n return report;\n }),\n };\n}\n\n// ─── Command ────────────────────────────────────────────────────────────────\n\nexport interface CheckOptions {\n inputs: string[];\n json: boolean;\n failUnder: number | undefined;\n noColor: boolean;\n autoDiscover: boolean;\n}\n\nexport async function runCheck(options: CheckOptions): Promise<void> {\n // oxlint-disable-next-line no-console -- CLI output\n const out = console.log;\n // oxlint-disable-next-line no-console -- CLI output\n const err = console.error;\n const colorEnabled = !options.noColor && process.stdout.isTTY === true;\n const c = createColors(colorEnabled);\n const files: string[] = [];\n\n for (const input of options.inputs) {\n const absPath = path.resolve(input);\n const stat = await fs.promises.stat(absPath).catch(() => null);\n\n if (!stat) {\n logger.error(`File not found: ${input}`);\n process.exit(1);\n }\n\n if (stat.isDirectory()) {\n logger.error(\"check command expects file paths, not directories.\");\n process.exit(1);\n }\n\n files.push(absPath);\n }\n\n const allJsonReports: JsonReport[] = [];\n let minCoverage = 100;\n let schemasProcessed = 0;\n\n for (const filePath of files) {\n const relPath = path.relative(process.cwd(), filePath);\n\n let schemas: DiscoveredSchema[];\n try {\n schemas = await discoverSchemas(filePath, { autoDiscover: options.autoDiscover });\n } catch (e) {\n logger.error(`Failed to load ${relPath}: ${getErrorMessage(e)}`);\n process.exit(1);\n }\n\n if (schemas.length === 0) {\n const hint = options.autoDiscover\n ? \"no Zod schemas exported\"\n : \"no compile() calls found (use --schemas auto to scan plain Zod exports)\";\n logger.warn(`${relPath}: ${hint}`);\n continue;\n }\n\n const results: { exportName: string; diagnostic: DiagnosticResult }[] = [];\n\n for (const s of schemas) {\n let ir: SchemaIR;\n try {\n // Collect refs exactly as the compile pipeline does. A callback that\n // captures is compiled by REFERENCE through an `__rf[]` entry, which\n // only happens when a ref table is present — extracting without one\n // reports every captured refine/transform and every superRefine as a\n // fallback, so `check` claimed 0% on schemas `generate` compiles in\n // full and told the user to rewrite code that was already fast.\n const refEntries: RefEntry[] = [];\n ir = extractSchema(s.schema, refEntries);\n } catch (e) {\n // An unsupported zod fails every schema identically, and nothing after\n // it can compile: report it once and stop, rather than once per export\n // followed by a coverage table that says 0% everywhere.\n if (isUnsupportedZodVersionError(e)) {\n logger.error(getErrorMessage(e));\n process.exit(1);\n }\n err(\n `${c.red}error${c.reset} Failed to extract \"${s.exportName}\" in ${relPath}: ${getErrorMessage(e)}`,\n );\n continue;\n }\n const diagnostic = diagnoseSchema(ir);\n results.push({ exportName: s.exportName, diagnostic });\n schemasProcessed++;\n\n if (diagnostic.coveragePct < minCoverage) {\n minCoverage = diagnostic.coveragePct;\n }\n }\n\n if (options.json) {\n allJsonReports.push(buildJsonReport(relPath, results));\n } else {\n out(`\\n${c.bold}${relPath}${c.reset}`);\n\n for (const { exportName, diagnostic } of results) {\n const pctColor = diagnostic.coveragePct === 100 ? c.green : c.yellow;\n const fpLabel = diagnostic.fastPathEligible\n ? `${c.green}Fast Path: eligible${c.reset}`\n : `${c.yellow}Fast Path: ineligible${c.reset}${c.dim} (${diagnostic.fastPathBlocker})${c.reset}`;\n\n out(\n `\\n ${c.cyan}${exportName}${c.reset} — ${pctColor}${diagnostic.coveragePct}%${c.reset} compiled (${diagnostic.compilable}/${diagnostic.total} nodes) | ${fpLabel}`,\n );\n\n const treeLines = renderTree(diagnostic.root, c, \" \");\n for (const line of treeLines) {\n out(line);\n }\n\n if (diagnostic.fallbacks.length > 0) {\n out(`\\n ${c.yellow}Fallbacks:${c.reset}`);\n for (const fb of diagnostic.fallbacks) {\n out(` ${c.red}✗${c.reset} ${fb.path} — ${fb.reason}`);\n out(` ${c.dim}${fb.hint}${c.reset}`);\n }\n }\n }\n }\n }\n\n if (options.json) {\n out(JSON.stringify(allJsonReports, null, 2));\n }\n\n if (options.failUnder !== undefined) {\n if (schemasProcessed === 0) {\n logger.error(\"No schemas were analyzed — cannot satisfy --fail-under\");\n process.exit(1);\n }\n if (minCoverage < options.failUnder) {\n logger.error(`Coverage ${minCoverage}% is below threshold ${options.failUnder}%`);\n process.exit(1);\n }\n } else if (schemasProcessed === 0) {\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;AAgBA,SAAS,WAAW,MAAsB,GAAW,SAAS,IAAI,SAAS,MAAgB;CACzF,MAAM,QAAkB,CAAC;CACzB,MAAM,YAAY,WAAW,KAAK,KAAK,SAAS,QAAQ;CACxD,MAAM,cAAc,WAAW,KAAK,KAAK,UAAU,SAAS,QAAQ;CAEpE,MAAM,aAAa,KAAK,WAAW,aAAa,GAAG,EAAE,MAAM,GAAG,EAAE,UAAU,GAAG,EAAE,IAAI,GAAG,EAAE;CACxF,MAAM,YAAY,GAAG,EAAE,OAAO,KAAK,OAAO,EAAE;CAC5C,MAAM,YAAY,KAAK,QAAQ,KAAK,SAAS,WAAW,IAAI,EAAE,MAAM,KAAK,OAAO,EAAE,UAAU;CAC5F,MAAM,cACJ,KAAK,WAAW,cAAc,KAAK,SAAS,IAAI,EAAE,OAAO,GAAG,KAAK,OAAO,GAAG,EAAE,UAAU;CAEzF,MAAM,KAAK,GAAG,SAAS,YAAY,WAAW,GAAG,YAAY,YAAY,aAAa;CAEtF,IAAI,KAAK,WAAW,cAAc,KAAK,MAAM;EAC3C,MAAM,aAAa,GAAG,YAAY;EAClC,MAAM,KAAK,GAAG,aAAa,EAAE,IAAI,QAAQ,KAAK,OAAO,EAAE,OAAO;CAChE;CAEA,KAAK,MAAM,CAAC,GAAG,UAAU,KAAK,SAAS,QAAQ,GAAG;EAChD,MAAM,OAAO,MAAM,KAAK,SAAS,SAAS;EAC1C,MAAM,KAAK,GAAG,WAAW,OAAO,GAAG,aAAa,IAAI,CAAC;CACvD;CAEA,OAAO;AACT;AAgBA,SAAS,gBACP,UACA,SACY;CACZ,OAAO;EACL,MAAM;EACN,SAAS,QAAQ,KAAK,EAAE,YAAY,iBAAiB;GACnD,MAAM,SAA2B;IAC/B;IACA,UAAU;KACR,OAAO,WAAW;KAClB,YAAY,WAAW;KACvB,SAAS,WAAW;IACtB;IACA,UAAU,EAAE,UAAU,WAAW,iBAAiB;IAClD,WAAW,WAAW;GACxB;GACA,IAAI,WAAW,iBACb,OAAO,SAAS,UAAU,WAAW;GAEvC,OAAO;EACT,CAAC;CACH;AACF;AAYA,eAAsB,SAAS,SAAsC;CAEnE,MAAM,MAAM,QAAQ;CAEpB,MAAM,MAAM,QAAQ;CAEpB,MAAM,IAAI,aADW,CAAC,QAAQ,WAAW,QAAQ,OAAO,UAAU,IAC/B;CACnC,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,QAAQ,QAAQ;EAClC,MAAM,UAAU,KAAK,QAAQ,KAAK;EAClC,MAAM,OAAO,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,CAAC,YAAY,IAAI;EAE7D,IAAI,CAAC,MAAM;GACT,OAAO,MAAM,mBAAmB,OAAO;GACvC,QAAQ,KAAK,CAAC;EAChB;EAEA,IAAI,KAAK,YAAY,GAAG;GACtB,OAAO,MAAM,oDAAoD;GACjE,QAAQ,KAAK,CAAC;EAChB;EAEA,MAAM,KAAK,OAAO;CACpB;CAEA,MAAM,iBAA+B,CAAC;CACtC,IAAI,cAAc;CAClB,IAAI,mBAAmB;CAEvB,KAAK,MAAM,YAAY,OAAO;EAC5B,MAAM,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,QAAQ;EAErD,IAAI;EACJ,IAAI;GACF,UAAU,MAAM,gBAAgB,UAAU,EAAE,cAAc,QAAQ,aAAa,CAAC;EAClF,SAAS,GAAG;GACV,OAAO,MAAM,kBAAkB,QAAQ,IAAI,gBAAgB,CAAC,GAAG;GAC/D,QAAQ,KAAK,CAAC;EAChB;EAEA,IAAI,QAAQ,WAAW,GAAG;GACxB,MAAM,OAAO,QAAQ,eACjB,4BACA;GACJ,OAAO,KAAK,GAAG,QAAQ,IAAI,MAAM;GACjC;EACF;EAEA,MAAM,UAAkE,CAAC;EAEzE,KAAK,MAAM,KAAK,SAAS;GACvB,IAAI;GACJ,IAAI;IAQF,KAAK,cAAc,EAAE,QAAQ,CAAS,CAAC;GACzC,SAAS,GAAG;IAIV,IAAI,6BAA6B,CAAC,GAAG;KACnC,OAAO,MAAM,gBAAgB,CAAC,CAAC;KAC/B,QAAQ,KAAK,CAAC;IAChB;IACA,IACE,GAAG,EAAE,IAAI,OAAO,EAAE,MAAM,sBAAsB,EAAE,WAAW,OAAO,QAAQ,IAAI,gBAAgB,CAAC,GACjG;IACA;GACF;GACA,MAAM,aAAa,eAAe,EAAE;GACpC,QAAQ,KAAK;IAAE,YAAY,EAAE;IAAY;GAAW,CAAC;GACrD;GAEA,IAAI,WAAW,cAAc,aAC3B,cAAc,WAAW;EAE7B;EAEA,IAAI,QAAQ,MACV,eAAe,KAAK,gBAAgB,SAAS,OAAO,CAAC;OAChD;GACL,IAAI,KAAK,EAAE,OAAO,UAAU,EAAE,OAAO;GAErC,KAAK,MAAM,EAAE,YAAY,gBAAgB,SAAS;IAChD,MAAM,WAAW,WAAW,gBAAgB,MAAM,EAAE,QAAQ,EAAE;IAC9D,MAAM,UAAU,WAAW,mBACvB,GAAG,EAAE,MAAM,qBAAqB,EAAE,UAClC,GAAG,EAAE,OAAO,uBAAuB,EAAE,QAAQ,EAAE,IAAI,IAAI,WAAW,gBAAgB,GAAG,EAAE;IAE3F,IACE,OAAO,EAAE,OAAO,aAAa,EAAE,MAAM,KAAK,WAAW,WAAW,YAAY,GAAG,EAAE,MAAM,aAAa,WAAW,WAAW,GAAG,WAAW,MAAM,YAAY,SAC5J;IAEA,MAAM,YAAY,WAAW,WAAW,MAAM,GAAG,MAAM;IACvD,KAAK,MAAM,QAAQ,WACjB,IAAI,IAAI;IAGV,IAAI,WAAW,UAAU,SAAS,GAAG;KACnC,IAAI,SAAS,EAAE,OAAO,YAAY,EAAE,OAAO;KAC3C,KAAK,MAAM,MAAM,WAAW,WAAW;MACrC,IAAI,SAAS,EAAE,IAAI,GAAG,EAAE,MAAM,GAAG,GAAG,KAAK,KAAK,GAAG,QAAQ;MACzD,IAAI,WAAW,EAAE,MAAM,GAAG,OAAO,EAAE,OAAO;KAC5C;IACF;GACF;EACF;CACF;CAEA,IAAI,QAAQ,MACV,IAAI,KAAK,UAAU,gBAAgB,MAAM,CAAC,CAAC;CAG7C,IAAI,QAAQ,cAAc,KAAA,GAAW;EACnC,IAAI,qBAAqB,GAAG;GAC1B,OAAO,MAAM,wDAAwD;GACrE,QAAQ,KAAK,CAAC;EAChB;EACA,IAAI,cAAc,QAAQ,WAAW;GACnC,OAAO,MAAM,YAAY,YAAY,uBAAuB,QAAQ,UAAU,EAAE;GAChF,QAAQ,KAAK,CAAC;EAChB;CACF,OAAO,IAAI,qBAAqB,GAC9B,QAAQ,KAAK,CAAC;AAElB"}
|
|
@@ -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":";;;;iBAuMgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBAmFtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRuntimeHelper, emitTemp, escapeString, hasMutation, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput } from "./context.js";
|
|
2
|
-
import { ZC_HOP_DECL, ZC_PLAIN_DECL, ZC_PROTO_SCRUB_DECL } from "./issue-decls.js";
|
|
2
|
+
import { ZC_ASYNC_DECL, ZC_HOP_DECL, ZC_PLAIN_DECL, ZC_PROTO_SCRUB_DECL } from "./issue-decls.js";
|
|
3
3
|
import { defaultValueExpr, needsPostInnerDefault } from "./schemas/default.js";
|
|
4
4
|
import { detectUnionDiscriminator } from "./schemas/discriminated-union.js";
|
|
5
|
-
import { estimateFastCost, predictedInlineSize } from "./fast-size.js";
|
|
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
8
|
import { fastStringCheck } from "./schemas/string.js";
|
|
@@ -220,7 +220,11 @@ function emitFailSentinel(ctx) {
|
|
|
220
220
|
* and 354 KB on the deep fixtures — far past the bytecode size where V8 stops
|
|
221
221
|
* running TurboFan on it, which would forfeit the speed this path exists for.
|
|
222
222
|
*/
|
|
223
|
-
function build(ir, input,
|
|
223
|
+
function build(ir, input, caller) {
|
|
224
|
+
const g = caller.discSkipKey !== void 0 && ir.type !== "object" && ir.type !== "readonly" && ir.type !== "zodDelegate" ? {
|
|
225
|
+
...caller,
|
|
226
|
+
discSkipKey: void 0
|
|
227
|
+
} : caller;
|
|
224
228
|
if (ir.type === "recursiveRef") return buildRecursiveCall(ir.refId ?? 0, input, g);
|
|
225
229
|
if (ir.type === "recursionTarget") return buildRecursionTarget(ir, input, g);
|
|
226
230
|
if (!g.rebuilds.has(ir)) return passthrough(ir, input, g);
|
|
@@ -246,8 +250,56 @@ function build(ir, input, g) {
|
|
|
246
250
|
if (out !== null) g.scope.used = before + out.code.length;
|
|
247
251
|
return out;
|
|
248
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* The order an object's properties are validated in: cheapest first, as the
|
|
255
|
+
* fast path orders its conjuncts (see estimateRuntimeCost), so rejected input
|
|
256
|
+
* is decided by a type guard rather than by the most expensive check that
|
|
257
|
+
* happens to be declared first — `safeParse` on a payload with a wrong boolean
|
|
258
|
+
* used to cost the email scan before it looked at the boolean. Accepted input
|
|
259
|
+
* runs every property whatever the order, and the output literal is assembled
|
|
260
|
+
* in shape order regardless, so a passing parse is unchanged.
|
|
261
|
+
*
|
|
262
|
+
* Properties whose parse can be OBSERVED to run are pinned: they keep their
|
|
263
|
+
* declaration order among themselves and run after every free property, so
|
|
264
|
+
* two transforms sharing state still fire in zod's order on a successful
|
|
265
|
+
* parse, and a rejected one fires neither (a failing parse never guaranteed
|
|
266
|
+
* that its effects ran — this pass bails at the first bad check). What counts
|
|
267
|
+
* as observable is user code and value construction: a transform or
|
|
268
|
+
* preprocess, a `.default()` (a function default is invoked on read), a
|
|
269
|
+
* `.catch()`, a coercion (`Number(x)` reaches a `valueOf`), an overwrite, a
|
|
270
|
+
* delegate, and a recursion edge, whose target is not inspected. A `.refine()`
|
|
271
|
+
* is not pinned — it is a predicate, and the fast path already reorders it.
|
|
272
|
+
*/
|
|
273
|
+
function orderedForBuild(properties, ctx) {
|
|
274
|
+
const pinned = properties.filter(([, propIR]) => orderSensitive(propIR));
|
|
275
|
+
if (pinned.length === properties.length) return properties;
|
|
276
|
+
return [...orderByRuntimeCost(properties.filter(([, propIR]) => !orderSensitive(propIR)), ([, propIR]) => propIR, ctx), ...pinned];
|
|
277
|
+
}
|
|
278
|
+
/** Does running this subtree do anything a caller could observe besides the verdict? */
|
|
279
|
+
function orderSensitive(ir) {
|
|
280
|
+
switch (ir.type) {
|
|
281
|
+
case "effect":
|
|
282
|
+
case "default":
|
|
283
|
+
case "catch":
|
|
284
|
+
case "fallback":
|
|
285
|
+
case "zodDelegate":
|
|
286
|
+
case "recursiveRef":
|
|
287
|
+
case "recursionTarget": return true;
|
|
288
|
+
case "string":
|
|
289
|
+
if (ir.coerce === true || ir.checks.some((c) => c.kind === "overwrite_effect")) return true;
|
|
290
|
+
break;
|
|
291
|
+
case "number":
|
|
292
|
+
case "boolean":
|
|
293
|
+
case "bigint":
|
|
294
|
+
case "date":
|
|
295
|
+
if (ir.coerce === true) return true;
|
|
296
|
+
break;
|
|
297
|
+
default: break;
|
|
298
|
+
}
|
|
299
|
+
return children(ir).some(orderSensitive);
|
|
300
|
+
}
|
|
249
301
|
/** Host `ir`'s build in its own function over a fresh parameter; returns its name. */
|
|
250
|
-
function hostBuild(ir, g) {
|
|
302
|
+
function hostBuild(ir, g, discSkipKey) {
|
|
251
303
|
const param = emitTemp(g.ctx, "bp");
|
|
252
304
|
const scope = {
|
|
253
305
|
temps: [],
|
|
@@ -256,7 +308,8 @@ function hostBuild(ir, g) {
|
|
|
256
308
|
const inner = build(ir, param, {
|
|
257
309
|
...g,
|
|
258
310
|
extractable: false,
|
|
259
|
-
scope
|
|
311
|
+
scope,
|
|
312
|
+
discSkipKey
|
|
260
313
|
});
|
|
261
314
|
if (inner === null) return null;
|
|
262
315
|
const name = emitTemp(g.ctx, "vb");
|
|
@@ -415,7 +468,7 @@ function buildDispatch(discriminator, cases, options, input, g) {
|
|
|
415
468
|
if (fn === void 0) {
|
|
416
469
|
const option = options[index];
|
|
417
470
|
if (option === void 0) return null;
|
|
418
|
-
const hosted = g.rebuilds.has(option) ? hostBuild(option, g) : hostPassthrough(option, g);
|
|
471
|
+
const hosted = g.rebuilds.has(option) ? hostBuild(option, g, discriminator) : hostPassthrough(option, g, discriminator);
|
|
419
472
|
if (hosted === null) return null;
|
|
420
473
|
fn = hosted;
|
|
421
474
|
hostedByOption.set(index, fn);
|
|
@@ -432,13 +485,13 @@ function buildDispatch(discriminator, cases, options, input, g) {
|
|
|
432
485
|
* Host a non-rebuilding option as `value-or-FAIL`, so a union can probe it with
|
|
433
486
|
* the same protocol as a rebuilding one.
|
|
434
487
|
*/
|
|
435
|
-
function hostPassthrough(ir, g) {
|
|
488
|
+
function hostPassthrough(ir, g, discSkipKey) {
|
|
436
489
|
const param = emitTemp(g.ctx, "bp");
|
|
437
490
|
const scope = {
|
|
438
491
|
temps: [],
|
|
439
492
|
used: 0
|
|
440
493
|
};
|
|
441
|
-
const expr = generateFast(ir, createFastGen(param, g.ctx, true, scope));
|
|
494
|
+
const expr = generateFast(ir, createFastGen(param, g.ctx, true, scope, discSkipKey));
|
|
442
495
|
if (expr === null) return null;
|
|
443
496
|
const name = emitTemp(g.ctx, "vp");
|
|
444
497
|
g.ctx.preamble.push(`function ${name}(${param}){${declareFastTemps(scope)}return ${expr === "true" ? param : `(${expr})?${param}:${g.fail}`};}`);
|
|
@@ -451,7 +504,7 @@ function hostPassthrough(ir, g) {
|
|
|
451
504
|
* key is present, so the ordinary value is still returned by reference.
|
|
452
505
|
*/
|
|
453
506
|
function passthrough(ir, input, g) {
|
|
454
|
-
const expr = generateFast(ir, createFastGen(input, g.ctx, true, g.scope));
|
|
507
|
+
const expr = generateFast(ir, createFastGen(input, g.ctx, true, g.scope, g.discSkipKey));
|
|
455
508
|
if (expr === null) return null;
|
|
456
509
|
const guard = expr === "true" ? "" : `if(!(${expr}))return ${g.fail};`;
|
|
457
510
|
if (!needsProtoScrub(ir)) return {
|
|
@@ -496,26 +549,41 @@ function buildObject(ir, input, g) {
|
|
|
496
549
|
const nonoptional = new Set(ir.nonoptionalKeys ?? []);
|
|
497
550
|
const refines = ir.checks ?? [];
|
|
498
551
|
if (refines.some((check) => check.kind !== "refine_effect")) return null;
|
|
499
|
-
|
|
552
|
+
const skipKey = g.discSkipKey;
|
|
553
|
+
const childGen = skipKey === void 0 ? g : {
|
|
554
|
+
...g,
|
|
555
|
+
discSkipKey: void 0
|
|
556
|
+
};
|
|
557
|
+
let code = skipKey === void 0 ? `if(typeof ${input}!=="object"||${input}===null||Array.isArray(${input}))return ${g.fail};` : "";
|
|
500
558
|
if (ir.strict === true) {
|
|
501
559
|
const keyVar = local(g, "bk");
|
|
502
560
|
code += `for(${keyVar} in ${input}){if(!(${keyMembershipTest(g.ctx, Object.keys(ir.properties), keyVar)}))return ${g.fail};}`;
|
|
503
561
|
}
|
|
504
|
-
const
|
|
505
|
-
|
|
562
|
+
const properties = parsedProperties(ir);
|
|
563
|
+
const built = /* @__PURE__ */ new Map();
|
|
564
|
+
for (const [key, propIR] of orderedForBuild(properties, g.ctx)) {
|
|
506
565
|
const keyStr = escapeString(key);
|
|
507
566
|
const slot = local(g, "bv");
|
|
567
|
+
if (key === skipKey && !g.rebuilds.has(propIR)) {
|
|
568
|
+
code += `${slot}=${input}[${keyStr}];`;
|
|
569
|
+
built.set(key, {
|
|
570
|
+
code: "",
|
|
571
|
+
value: slot
|
|
572
|
+
});
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
508
575
|
if (nonoptional.has(key)) code += `if(!(${keyStr} in ${input}))return ${g.fail};`;
|
|
509
576
|
code += `${slot}=${input}[${keyStr}];`;
|
|
510
|
-
const propBuilt = build(propIR, slot,
|
|
577
|
+
const propBuilt = build(propIR, slot, childGen);
|
|
511
578
|
if (propBuilt === null) return null;
|
|
512
579
|
code += propBuilt.code;
|
|
513
|
-
|
|
514
|
-
always: outputAlwaysDefined(propIR),
|
|
515
|
-
keyStr,
|
|
516
|
-
value: propBuilt.value
|
|
517
|
-
});
|
|
580
|
+
built.set(key, propBuilt);
|
|
518
581
|
}
|
|
582
|
+
const slots = properties.map(([key, propIR]) => ({
|
|
583
|
+
always: outputAlwaysDefined(propIR),
|
|
584
|
+
keyStr: escapeString(key),
|
|
585
|
+
value: built.get(key).value
|
|
586
|
+
}));
|
|
519
587
|
const out = local(g, "bo");
|
|
520
588
|
const literal = [];
|
|
521
589
|
let appends = "";
|
|
@@ -529,12 +597,23 @@ function buildObject(ir, input, g) {
|
|
|
529
597
|
appends += slot.always ? `${out}[${slot.keyStr}]=${slot.value};` : `if(${slot.value}!==undefined||(${slot.keyStr} in ${input})){${out}[${slot.keyStr}]=${slot.value};}`;
|
|
530
598
|
}
|
|
531
599
|
code += `${out}={${literal.join(",")}};${appends}`;
|
|
532
|
-
for (const check of refines) code +=
|
|
600
|
+
for (const check of refines) code += buildRefine(check, out, g);
|
|
533
601
|
return {
|
|
534
602
|
code,
|
|
535
603
|
value: out
|
|
536
604
|
};
|
|
537
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* A `.refine()` predicate on the build path: FAIL on a falsy verdict, and
|
|
608
|
+
* zod's synchronous-parse error on a Promise — which is truthy, so an
|
|
609
|
+
* unguarded test would accept the input (see ZC_ASYNC_DECL). The verdict is
|
|
610
|
+
* bound to a local so the Promise test reads it once.
|
|
611
|
+
*/
|
|
612
|
+
function buildRefine(check, value, g) {
|
|
613
|
+
const asy = emitRuntimeHelper(g.ctx, "__zcAsy", ZC_ASYNC_DECL);
|
|
614
|
+
const verdict = local(g, "br");
|
|
615
|
+
return `${verdict}=${emitEffectCallable(g.ctx, check)}(${value});if(${verdict} instanceof Promise)${asy}();if(!${verdict})return ${g.fail};`;
|
|
616
|
+
}
|
|
538
617
|
function buildArray(ir, input, g) {
|
|
539
618
|
let sizes = "";
|
|
540
619
|
const refines = [];
|
|
@@ -559,7 +638,7 @@ function buildArray(ir, input, g) {
|
|
|
559
638
|
const inner = build(ir.element, elem, g);
|
|
560
639
|
if (inner === null) return null;
|
|
561
640
|
let code = `if(!Array.isArray(${input}))return ${g.fail};` + sizes + `${out}=new Array(${input}.length);for(${index}=0;${index}<${input}.length;${index}++){${elem}=${input}[${index}];${inner.code}${out}[${index}]=${inner.value};}`;
|
|
562
|
-
for (const check of refines) code +=
|
|
641
|
+
for (const check of refines) code += buildRefine(check, out, g);
|
|
563
642
|
return {
|
|
564
643
|
code,
|
|
565
644
|
value: out
|
|
@@ -610,12 +689,13 @@ function buildRecord(ir, input, g) {
|
|
|
610
689
|
* (see extractPipe), so there is no parse context to reproduce.
|
|
611
690
|
*/
|
|
612
691
|
function buildEffect(ir, input, g) {
|
|
692
|
+
const asy = emitRuntimeHelper(g.ctx, "__zcAsy", ZC_ASYNC_DECL);
|
|
613
693
|
if (ir.effectKind === "preprocess") {
|
|
614
694
|
const value = local(g, "bpv");
|
|
615
695
|
const inner = build(ir.inner, value, g);
|
|
616
696
|
if (inner === null) return null;
|
|
617
697
|
return {
|
|
618
|
-
code: `${value}=${emitEffectCallable(g.ctx, ir)}(${input});${inner.code}`,
|
|
698
|
+
code: `${value}=${emitEffectCallable(g.ctx, ir)}(${input});if(${value} instanceof Promise)${asy}();${inner.code}`,
|
|
619
699
|
value: inner.value
|
|
620
700
|
};
|
|
621
701
|
}
|
|
@@ -623,7 +703,7 @@ function buildEffect(ir, input, g) {
|
|
|
623
703
|
if (inner === null) return null;
|
|
624
704
|
const out = local(g, "bx");
|
|
625
705
|
return {
|
|
626
|
-
code: `${inner.code}${out}=${emitEffectCallable(g.ctx, ir)}(${inner.value});`,
|
|
706
|
+
code: `${inner.code}${out}=${emitEffectCallable(g.ctx, ir)}(${inner.value});if(${out} instanceof Promise)${asy}();`,
|
|
627
707
|
value: out
|
|
628
708
|
};
|
|
629
709
|
}
|
|
@@ -647,7 +727,7 @@ function buildString(ir, input, g) {
|
|
|
647
727
|
code += `${value}=${emitEffectFn(g.ctx, check.source)}(${value});`;
|
|
648
728
|
break;
|
|
649
729
|
case "refine_effect":
|
|
650
|
-
code +=
|
|
730
|
+
code += buildRefine(check, value, g);
|
|
651
731
|
break;
|
|
652
732
|
case "super_refine_effect": return null;
|
|
653
733
|
default: {
|
|
@@ -750,15 +830,14 @@ function buildCoercedPrimitive(ir, input, g) {
|
|
|
750
830
|
* `slowDefault` emits, reading the value off the retained schema so a
|
|
751
831
|
* reference-typed default keeps zod's identity (one shared object, not a copy).
|
|
752
832
|
*
|
|
753
|
-
* The substituted value is not validated
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
833
|
+
* The substituted value is not validated. The by-reference fast form refuses
|
|
834
|
+
* `undefined` for that reason (it would hand the input back, not the default);
|
|
835
|
+
* the acceptance form `.is()` runs accepts it, as the schema does — see
|
|
836
|
+
* fastDefault.
|
|
757
837
|
*/
|
|
758
838
|
function buildDefault(ir, input, g) {
|
|
759
839
|
const inner = build(ir.inner, input, g);
|
|
760
840
|
if (inner === null) return null;
|
|
761
|
-
g.ctx.buildSubstitutesValue = true;
|
|
762
841
|
const out = local(g, "bq");
|
|
763
842
|
const value = defaultValueExpr(ir);
|
|
764
843
|
const reapply = needsPostInnerDefault(ir) ? `if(${out}===undefined){${out}=${value};}` : "";
|