From 3d1f008ae381dec01751725b7422985c84c74aae Mon Sep 17 00:00:00 2001 From: Svend Date: Sat, 3 Oct 2026 02:33:10 +0000 Subject: [PATCH] fix(db): use static zod lookup in toZodSchema so locales tree-shake (#10866) Co-authored-by: Taesu <166604494+bytaesu@users.noreply.github.com> Co-authored-by: Taesu --- .changeset/organization-zod-locales.md | 5 +++ e2e/smoke/test/esbuild.spec.ts | 45 +++++++++++++++++++ .../test/fixtures/esbuild/src/organization.ts | 4 ++ packages/better-auth/src/db/to-zod.test.ts | 18 ++++++++ packages/better-auth/src/db/to-zod.ts | 13 +++++- 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 .changeset/organization-zod-locales.md create mode 100644 e2e/smoke/test/fixtures/esbuild/src/organization.ts diff --git a/.changeset/organization-zod-locales.md b/.changeset/organization-zod-locales.md new file mode 100644 index 0000000000..fa042ec26b --- /dev/null +++ b/.changeset/organization-zod-locales.md @@ -0,0 +1,5 @@ +--- +"better-auth": patch +--- + +The organization plugin no longer pulls every zod locale into server bundles. diff --git a/e2e/smoke/test/esbuild.spec.ts b/e2e/smoke/test/esbuild.spec.ts index ed9d334b03..ff0d58024f 100644 --- a/e2e/smoke/test/esbuild.spec.ts +++ b/e2e/smoke/test/esbuild.spec.ts @@ -68,6 +68,51 @@ it("build minimal without unexpected imports", async () => { ); }); +/** + * @see https://github.com/better-auth/better-auth/issues/10865 + */ +it("build organization plugin without unused zod locales", async () => { + const esbuildDir = join(fixturesDir, "esbuild"); + const buildProcess = spawn( + "pnpm", + [ + "exec", + "esbuild", + "src/organization.ts", + "--bundle", + "--outfile=dist/organization.js", + ], + { + cwd: esbuildDir, + stdio: ["ignore", "inherit", "inherit"], + }, + ); + await new Promise((resolve, reject) => { + buildProcess.once("error", reject); + buildProcess.once("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject( + new Error(`esbuild organization build failed with code ${code}`), + ); + } + }); + }); + const outputFile = join(esbuildDir, "dist", "organization.js"); + const outputContent = await readFile(outputFile, "utf-8"); + const locales = outputContent.match(/zod\/v4\/locales\/[\w-]+\.js/g) ?? []; + const unusedLocales = locales.filter( + (locale) => + locale !== "zod/v4/locales/en.js" && locale !== "zod/v4/locales/index.js", + ); + assert.deepStrictEqual( + unusedLocales, + [], + "Built output should not contain unused zod locales", + ); +}); + /** * @see https://github.com/better-auth/better-auth/issues/10366 */ diff --git a/e2e/smoke/test/fixtures/esbuild/src/organization.ts b/e2e/smoke/test/fixtures/esbuild/src/organization.ts new file mode 100644 index 0000000000..df10a366e8 --- /dev/null +++ b/e2e/smoke/test/fixtures/esbuild/src/organization.ts @@ -0,0 +1,4 @@ +import { betterAuth } from "better-auth/minimal"; +import { organization } from "better-auth/plugins"; + +export const auth = betterAuth({ plugins: [organization()] }); diff --git a/packages/better-auth/src/db/to-zod.test.ts b/packages/better-auth/src/db/to-zod.test.ts index a09f2dd7b5..a4d57a8fad 100644 --- a/packages/better-auth/src/db/to-zod.test.ts +++ b/packages/better-auth/src/db/to-zod.test.ts @@ -33,6 +33,24 @@ describe("toZodSchema", () => { }); }); + describe("scalar field types", () => { + it("should map each scalar type to its zod schema", () => { + const schema = toZodSchema({ + fields: { + name: { type: "string" }, + age: { type: "number" }, + active: { type: "boolean" }, + createdAt: { type: "date" }, + }, + isClientSide: true, + }); + + const valid = { name: "a", age: 1, active: true, createdAt: new Date() }; + expect(schema.parse(valid)).toEqual(valid); + expect(schema.safeParse({ ...valid, age: "1" }).success).toBe(false); + }); + }); + describe("required: false field nullability", () => { it("should accept null, undefined, and a value for an optional field", () => { const schema = toZodSchema({ diff --git a/packages/better-auth/src/db/to-zod.ts b/packages/better-auth/src/db/to-zod.ts index 05eb086b79..7b05aee123 100644 --- a/packages/better-auth/src/db/to-zod.ts +++ b/packages/better-auth/src/db/to-zod.ts @@ -2,6 +2,17 @@ import type { DBFieldAttribute } from "@better-auth/core/db"; import type { ZodType } from "zod"; import * as z from "zod"; +/** + * Static lookup keeps `zod` tree-shakeable. `z[field.type]()` forces bundlers + * to retain every `zod` export, including all locales. + */ +const scalarSchemas = { + string: z.string, + number: z.number, + boolean: z.boolean, + date: z.date, +}; + export function toZodSchema< Fields extends Record, IsClientSide extends boolean, @@ -32,7 +43,7 @@ export function toZodSchema< } else if (Array.isArray(field.type)) { schema = z.any(); } else { - schema = z[field.type](); + schema = scalarSchemas[field.type](); } if (field?.required === false) {