Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
105 changes: 104 additions & 1 deletion packages/zod/src/v4/classic/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, expectTypeOf, test } from "vitest";
import { expect, expectTypeOf, test, vi } from "vitest";
import * as z from "zod/v4";
import type { util } from "zod/v4/core";
import { Doc } from "../../core/doc.js";

test("z.boolean", () => {
const a = z.boolean();
Expand Down Expand Up @@ -818,6 +819,108 @@ test("isPlainObject", () => {
expect(z.core.util.isPlainObject({ constructor: [] })).toEqual(true);
});

const loadAllowsEvalValue = async () => {
const mod = await import("../../core/util.js");
return mod.allowsEval.value;
};

const loadAllowsEvalValueFresh = async () => {
vi.resetModules();
return loadAllowsEvalValue();
};

test("Cloudflare Workers can use fast path when eval is available", async () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
Object.defineProperty(globalThis, "navigator", {
value: { userAgent: "Cloudflare-Workers" },
configurable: true,
writable: true,
});

try {
const allowsEval = await loadAllowsEvalValueFresh();
expect(allowsEval).toEqual(true);
} finally {
if (descriptor) {
Object.defineProperty(globalThis, "navigator", descriptor);
} else {
delete (globalThis as any).navigator;
}
}
});

test("falls back gracefully when eval is disabled", async () => {
try {
vi.stubGlobal("Function", function ThrowingFunction() {
throw new Error("Function constructor disabled");
});

const allowsEval = await loadAllowsEvalValue();
expect(allowsEval).toEqual(false);
} finally {
vi.unstubAllGlobals();
}
});

test("object fast path falls back when JIT compilation is blocked at call site", () => {
const schema = z.object({ a: z.string() });

const compileSpy = vi.spyOn(Doc.prototype, "compile").mockImplementation(() => {
throw new Error("Function constructor disabled by environment");
});

expect(schema.parse({ a: "hello" })).toEqual({ a: "hello" });

compileSpy.mockRestore();
});

test("allowsEval caches for non-Cloudflare environments", async () => {
const first = await loadAllowsEvalValue();

try {
vi.stubGlobal("Function", function ThrowingFunction() {
throw new Error("Function constructor disabled");
});

const second = await loadAllowsEvalValue();
expect(second).toEqual(first);
} finally {
vi.unstubAllGlobals();
vi.resetModules();
}
});

test("Cloudflare user agent always re-evaluates Function availability", async () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
Object.defineProperty(globalThis, "navigator", {
value: { userAgent: "Cloudflare-Workers" },
configurable: true,
writable: true,
});

try {
const first = await loadAllowsEvalValue();

vi.stubGlobal("Function", function ThrowingFunction() {
throw new Error("Function constructor disabled");
});

const second = await loadAllowsEvalValue();
expect(second).not.toEqual(first);
expect(second).toEqual(false);
} finally {
vi.unstubAllGlobals();

if (descriptor) {
Object.defineProperty(globalThis, "navigator", descriptor);
} else {
delete (globalThis as any).navigator;
}

vi.resetModules();
}
});

test("shallowClone with constructor field", () => {
const objWithConstructor = { constructor: "string", key: "value" };
const cloned = z.core.util.shallowClone(objWithConstructor);
Expand Down
30 changes: 22 additions & 8 deletions packages/zod/src/v4/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1967,12 +1967,10 @@ export const $ZodObjectJIT: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$
};

let fastpass!: ReturnType<typeof generateFastpass>;
let fastpassError: unknown | null = null;

const isObject = util.isObject;
const jit = !core.globalConfig.jitless;
const allowsEval = util.allowsEval;

const fastEnabled = jit && allowsEval.value; // && !def.catchall;
const catchall = def.catchall;

let value!: typeof _normalized.value;
Expand All @@ -1990,13 +1988,29 @@ export const $ZodObjectJIT: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$
return payload;
}

if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) {
const canFastpass =
jit && util.allowsEval.value && ctx?.async === false && ctx.jitless !== true && !fastpassError;

if (canFastpass) {
// always synchronous
if (!fastpass) fastpass = generateFastpass(def.shape);
payload = fastpass(payload, ctx);
if (!fastpass) {
try {
fastpass = generateFastpass(def.shape);
} catch (err) {
fastpassError = err;
}
}

if (!catchall) return payload;
return handleCatchall([], input, payload, ctx, value, inst);
if (fastpass) {
try {
payload = fastpass(payload, ctx);

if (!catchall) return payload;
return handleCatchall([], input, payload, ctx, value, inst);
} catch (err) {
fastpassError = err;
}
}
}

return superParse(payload, ctx);
Expand Down
28 changes: 21 additions & 7 deletions packages/zod/src/v4/core/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,20 +368,34 @@ export function isObject(data: any): data is Record<PropertyKey, unknown> {
return typeof data === "object" && data !== null && !Array.isArray(data);
}

export const allowsEval: { value: boolean } = cached(() => {
// @ts-ignore
if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) {
return false;
}

const canUseFunction = () => {
try {
const F = Function;
new F("");
return true;
} catch (_) {
return false;
}
});
};

export const allowsEval: { readonly value: boolean } = (() => {
let cached: boolean | undefined;

return {
get value() {
const ua = typeof navigator === "undefined" ? undefined : navigator?.userAgent;
const isCloudflare = typeof ua === "string" && ua.includes("Cloudflare");

if (isCloudflare) {
// Cloudflare Workers can vary by compat flag; always re-check.
return canUseFunction();
}
Comment on lines +386 to +392
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can check for this directly:

if (globalThis.Cloudflare?.compatibilityFlags?.allow_eval_during_startup)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah wait – I think I see the problem – even if that flag's enabled, this check might happen at a time other than startup... so you have to check regardless, and can't cache... right?


cached ??= canUseFunction();
return cached;
},
};
})();

export function isPlainObject(o: any): o is Record<PropertyKey, unknown> {
if (isObject(o) === false) return false;
Expand Down