Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
78ba4e4
transpiler: compile zod v4 schemas into lazy wrappers with flat valid…
robobun Aug 5, 2026
55bace0
zod transform: treat operators on pure operands as pure, allow any pu…
robobun Aug 5, 2026
6c3d1ac
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 5, 2026
42fe09d
zod transform: single-line comments for the flag plumbing sites
robobun Aug 5, 2026
29e201b
zod transform: address review feedback
robobun Aug 5, 2026
e95a971
zod transform: delegate on inconclusive union options and unconsumed …
robobun Aug 5, 2026
d45a2f1
zod transform: preserve negative-zero defaults, simplify the catchall…
robobun Aug 5, 2026
6f17ad1
zod transform: delegate optionals whose inner optionality is not stat…
robobun Aug 5, 2026
71001ac
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 5, 2026
35c1338
zod transform: only capture immutable bindings as runtime refs
robobun Aug 5, 2026
147d5b3
zod transform: defer pick/omit/partial/required when a mask key is no…
robobun Aug 5, 2026
7ee0efd
zod transform: do not capture imported bindings as runtime refs
robobun Aug 5, 2026
f2a34e8
zod transform: reject non-finite numeric literals and gate absent-key…
robobun Aug 5, 2026
7b2ae8a
zod transform: match zod's pick key order and keep literal([]) constr…
robobun Aug 5, 2026
ee1f05c
bundler: add --zod-compiler / zodCompiler to turn on the zod schema t…
robobun Aug 19, 2026
cc832f6
zod transform: read whole rope strings, and defer checks on booleans …
robobun Aug 19, 2026
b764b8b
zod compiler: shorten the option comments
robobun Aug 19, 2026
bc83547
zod tests: rename the prefix inputs
robobun Aug 19, 2026
b9602db
zod transform: leave macro-remapped imports alone, and ignore states …
robobun Aug 19, 2026
6cd0687
zod transform: a literal held by reference may be an array, so it nev…
robobun Aug 19, 2026
6fa6ebf
bundler: reserve global names only for the runtime helpers a chunk keeps
robobun Aug 28, 2026
f06d414
bundler: shorten the reserved-name comments
robobun Aug 28, 2026
290a809
zod transform: leave release-dependent and malformed inputs to zod, r…
robobun Aug 28, 2026
a466973
zod transform: a release-dependent input ends the fast path instead o…
robobun Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions completions/bun-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -3928,6 +3928,13 @@
"required": false,
"multiple": false
},
{
"name": "zod-compiler",
"description": "Compile zod v4 schemas into lazy wrappers with precompiled validators (experimental)",
"hasValue": false,
"required": false,
"multiple": false
},
{
"name": "no-bundle",
"description": "Transpile file only, do not bundle",
Expand Down
7 changes: 7 additions & 0 deletions docs/snippets/cli/build.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ bun build <entry points>
output mode from `--target` (`browser` → client, `bun`/`node` → ssr). Experimental.
</ParamField>

<ParamField path="--zod-compiler" type="boolean">
Compile zod v4 schemas at build time. A schema expression such as `z.object({ ... })` becomes a small wrapper whose
`parse` and `safeParse` run a validator compiled from the schema. The wrapper only constructs the real zod schema
when a parse fails or when anything else on it is used, so errors always come from zod itself. Schemas the
compiler cannot analyze are left as written. Experimental.
</ParamField>

### Standalone Executables

<ParamField path="--compile" type="boolean">
Expand Down
16 changes: 16 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3508,6 +3508,22 @@ declare module "bun" {
*/
reactCompilerOutputMode?: "client" | "ssr";

/**
* Compile zod v4 schemas at build time.
*
* A schema expression such as `z.object({ ... })` becomes a small wrapper
* whose `parse` and `safeParse` run a validator compiled from the schema.
* The wrapper only constructs the real zod schema when a parse fails or
* when anything else on it is used, so errors always come from zod itself.
* Schemas the compiler cannot analyze are left as written.
*
* Equivalent to the CLI `--zod-compiler` flag.
*
* @default false
* @experimental
*/
zodCompiler?: boolean;

/**
* A map of file paths to their contents for in-memory bundling.
*
Expand Down
4 changes: 3 additions & 1 deletion scripts/build/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,9 @@ function emitRuntimeJs({ n, cfg, o, dirStamp }: Ctx): void {
n.build({
outputs: [out],
rule: "esbuild",
inputs: [src],
// runtime.bun.js re-exports ./runtime; esbuild bundles both, so each is
// an input.
inputs: [src, resolve(cfg.cwd, "src", "runtime.js")],
implicitInputs: [o.rootInstall],
orderOnlyInputs: [dirStamp],
vars: {
Expand Down
12 changes: 9 additions & 3 deletions src/ast/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,11 @@ pub struct Imports {
pub(crate) __promiseAll: Ref,
pub(crate) __MEMO_CACHE_SENTINEL: Ref,
pub(crate) __EARLY_RETURN_SENTINEL: Ref,
pub(crate) __zod: Ref,
}

impl Imports {
pub const ALL: [&'static [u8]; 27] = [
pub const ALL: [&'static [u8]; 28] = [
b"__name",
b"__require",
b"__export",
Expand Down Expand Up @@ -204,12 +205,13 @@ impl Imports {
b"__promiseAll",
b"__MEMO_CACHE_SENTINEL",
b"__EARLY_RETURN_SENTINEL",
b"__zod",
];

/// Rust stable cannot sort in `const`; precomputed here and verified by
/// the test in `tests` below.
#[cfg_attr(not(test), allow(dead_code))]
const ALL_SORTED: [&'static [u8]; 27] = [
const ALL_SORTED: [&'static [u8]; 28] = [
b"$$typeof",
b"__EARLY_RETURN_SENTINEL",
b"__MEMO_CACHE_SENTINEL",
Expand Down Expand Up @@ -237,11 +239,12 @@ impl Imports {
b"__require",
b"__runInitializers",
b"__using",
b"__zod",
];

/// When generating the list of runtime imports, we sort it for determinism.
/// This is a lookup table so we don't need to resort the strings each time
pub const ALL_SORTED_INDEX: [usize; 27] = [
pub const ALL_SORTED_INDEX: [usize; 28] = [
15, // __name
24, // __require
7, // __export
Expand Down Expand Up @@ -269,6 +272,7 @@ impl Imports {
21, // __promiseAll
2, // __MEMO_CACHE_SENTINEL
1, // __EARLY_RETURN_SENTINEL
27, // __zod
];

pub const NAME: &'static [u8] = b"bun:wrap";
Expand Down Expand Up @@ -304,6 +308,7 @@ impl Imports {
24 => self.__promiseAll,
25 => self.__MEMO_CACHE_SENTINEL,
26 => self.__EARLY_RETURN_SENTINEL,
27 => self.__zod,
_ => return None,
};
r.to_nullable()
Expand Down Expand Up @@ -339,6 +344,7 @@ impl Imports {
24 => Some(&mut self.__promiseAll),
25 => Some(&mut self.__MEMO_CACHE_SENTINEL),
26 => Some(&mut self.__EARLY_RETURN_SENTINEL),
27 => Some(&mut self.__zod),
_ => None,
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ pub mod feature_flag {
// the client implementation matures. `--experimental-http3-fetch` is the
// CLI equivalent.
new_feature_flag!(pub BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP3_CLIENT, "BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP3_CLIENT", {});
// Zod schema transform (src/js_parser/zod.rs) in every transpiler; `bun build --zod-compiler` is the per-build switch.
new_feature_flag!(pub BUN_FEATURE_FLAG_EXPERIMENTAL_ZOD, "BUN_FEATURE_FLAG_EXPERIMENTAL_ZOD", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_FORCE_IO_POOL, "BUN_FEATURE_FLAG_FORCE_IO_POOL", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS, "BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS", {});
new_feature_flag!(pub BUN_INSTRUMENTS, "BUN_INSTRUMENTS", {});
Expand Down
4 changes: 4 additions & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2490,6 +2490,10 @@ pub mod parse_worker {
|| !(task.experimental_decorators || task.emit_decorator_metadata);
opts.features.unwrap_commonjs_packages = topts.unwrap_commonjs_packages;
opts.features.no_macros = topts.no_macros;
opts.features.zod_transform = topts.zod_compiler
|| bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_EXPERIMENTAL_ZOD
.get()
.unwrap_or(false);
// Modeled as
// `Option<Box<StringSet>>` on both sides, so we deep-clone (small —
// CLI-supplied flag set). PERF: retype
Expand Down
25 changes: 18 additions & 7 deletions src/bundler/linker_context/renameSymbolsInChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::bun_renamer as renamer;
use crate::bun_renamer::{ChunkRenamer, MinifyRenamer, NumberRenamer, StableSymbolCount};
use crate::chunk::Content;
use crate::js_meta;
use crate::{Chunk, LinkerContext, StableRef, WrapKind};
use crate::{Chunk, Index, LinkerContext, StableRef, WrapKind};

/// TODO: investigate if we need to parallelize this function
/// esbuild does parallelize it.
Expand Down Expand Up @@ -127,12 +127,23 @@ pub(crate) unsafe fn rename_symbols_in_chunk(

let mut reserved_names = renamer::compute_initial_reserved_names(c.options.output_format)?;
for &source_index in files_in_order {
renamer::compute_reserved_names_for_scope(
&all_module_scopes[source_index as usize],
// SAFETY: `symbols` points to the live `c.graph.symbols`; read-only here.
unsafe { &*symbols },
&mut reserved_names,
);
// SAFETY: `symbols` points to the live `c.graph.symbols`; read-only here.
let symbols_ref: &symbol::Map = unsafe { &*symbols };
if source_index == Index::RUNTIME.value() {
// Only the runtime helpers a chunk keeps may take a global's name away from user code.
renamer::compute_reserved_names_for_live_parts(
all_parts[source_index as usize].as_slice(),
&c.graph.parts_live[source_index as usize],
symbols_ref,
&mut reserved_names,
);
} else {
renamer::compute_reserved_names_for_scope(
&all_module_scopes[source_index as usize],
symbols_ref,
&mut reserved_names,
);
}
}

let sorted_imports_from_other_chunks: Vec<StableRef> = {
Expand Down
4 changes: 4 additions & 0 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,8 @@ pub struct BundleOptions<'a> {
pub react_fast_refresh: bool,
pub react_compiler: bun_ast::runtime::ReactCompilerMode,
pub react_compiler_parse_test_pragmas: bool,
/// `--zod-compiler` / `zodCompiler`: sets `RuntimeFeatures::zod_transform` for this build.
pub zod_compiler: bool,
pub(crate) inject: Option<Box<[Box<[u8]>]>>,
// `bun_url::URL<'a>` borrows its input string; the owned variant keeps the
// struct self-contained.
Expand Down Expand Up @@ -1434,6 +1436,7 @@ impl<'a> BundleOptions<'a> {
react_fast_refresh: self.react_fast_refresh,
react_compiler: self.react_compiler,
react_compiler_parse_test_pragmas: self.react_compiler_parse_test_pragmas,
zod_compiler: self.zod_compiler,
inject: self.inject.clone(),
origin: self.origin.clone(),
// The owning handle stays with the parent; copying it here would
Expand Down Expand Up @@ -1695,6 +1698,7 @@ impl<'a> BundleOptions<'a> {
react_fast_refresh: false,
react_compiler: bun_ast::runtime::ReactCompilerMode::Disabled,
react_compiler_parse_test_pragmas: false,
zod_compiler: false,
inject: None,
origin: bun_url::OwnedURL::from_href(Box::default()),
output_dir_handle: None,
Expand Down
4 changes: 4 additions & 0 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,10 @@ impl<'a> Transpiler<'a> {
opts.features.lower_using = !target.is_bun();

opts.features.inject_jest_globals = this_parse.inject_jest_globals;
opts.features.zod_transform = self.options.zod_compiler
|| bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_EXPERIMENTAL_ZOD
.get()
.unwrap_or(false);
opts.features.minify_syntax = self.options.minify_syntax;
opts.features.minify_identifiers = self.options.minify_identifiers;
opts.features.dead_code_elimination = self.options.dead_code_elimination;
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub(crate) mod repl_transforms;
pub mod scan;
pub mod typescript;
pub mod visit;
pub(crate) mod zod;

pub use p::P;
pub use parse::parse_entry::{Options as ParserOptions, Parser};
Expand Down
8 changes: 8 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
/// parser / visitor / lowerer body at startup.
pub(crate) jsx_transform: JSXTransformType,
pub(crate) macro_: MacroState<'a>,
pub(crate) zod: crate::zod::ZodState,
pub(crate) arena: &'a Bump,
pub(crate) options: ParserOptions<'a>,
/// Raw pointer alias of `lexer.log`. Rust cannot store two `&'a mut Log` to one allocation
Expand Down Expand Up @@ -3645,6 +3646,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if let Some(star) = stmt.star_name_loc.to_nullable() {
let name = self.load_name_from_ref(stmt.namespace_ref);
stmt.namespace_ref = self.declare_symbol(js_ast::symbol::Kind::Import, star, name)?;
self.zod_maybe_track_import(path.text, stmt.namespace_ref, None);

if Self::TRACK_SYMBOL_USAGE_DURING_PARSE_PASS {
if let Some(uses) = &mut self.parse_pass_symbol_uses {
Expand Down Expand Up @@ -3739,6 +3741,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

// A remapped binding is a macro, not zod.
self.zod_maybe_track_import(path.text, r#ref, Some(b"default"));

if Self::TRACK_SYMBOL_USAGE_DURING_PARSE_PASS {
if let Some(uses) = &mut self.parse_pass_symbol_uses {
uses.put(
Expand Down Expand Up @@ -3819,6 +3824,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

self.zod_maybe_track_import(path.text, r#ref, Some(alias));

if Self::TRACK_SYMBOL_USAGE_DURING_PARSE_PASS {
if let Some(uses) = &mut self.parse_pass_symbol_uses {
uses.put(
Expand Down Expand Up @@ -8648,6 +8655,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
to_expr_wrapper_hoisted: bun_ast::binding::ToExprWrapper::dangling(),
source,
macro_: MacroState::init(),
zod: crate::zod::ZodState::init(),
current_scope: scope,
module_scope: scope,
scopes_in_order: scope_order,
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ impl<'a> Options<'a> {
bundler_feature_flags: None,
repl_mode: f.repl_mode,
jsx_optimization_inline: f.jsx_optimization_inline,
zod_transform: f.zod_transform,
},
tree_shaking: self.tree_shaking,
bundle: self.bundle,
Expand Down
9 changes: 9 additions & 0 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ pub mod Runtime {
// Vestigial bool stub retained until its last reader (parseJSXElement.rs)
// is ported to the real predicate; defaults false and is otherwise inert.
pub jsx_optimization_inline: bool,

/// Zod schema transform; see `src/js_parser/zod.rs`.
pub zod_transform: bool,
}

impl Default for Features {
Expand Down Expand Up @@ -306,6 +309,7 @@ pub mod Runtime {
bundler_feature_flags: None,
repl_mode: false,
jsx_optimization_inline: false,
zod_transform: false,
}
}
}
Expand Down Expand Up @@ -391,6 +395,11 @@ pub mod Runtime {
hasher.update(b"\x00");
}
}

// Hashed only when enabled so existing cache entries stay valid.
if self.zod_transform {
hasher.update(b"zod_transform");
}
}

pub(crate) fn should_unwrap_require(&self, package_name: &[u8]) -> bool {
Expand Down
7 changes: 7 additions & 0 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

if p.options.features.zod_transform {
if let Some(result) = p.maybe_transform_zod_call(expr) {
*e = result;
return;
}
}

if matches!(e_.target.data, Data::ERequireCallTarget) {
e_.can_be_unwrapped_if_unused = E::CallUnwrap::Never;

Expand Down
Loading
Loading