diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8b6f7605..b0563d351 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,6 +87,8 @@ jobs: bun-version: 1.4.0 - run: pnpm install - run: pnpm run build + - name: Verify benchmark artifact has no uncommitted drift + run: git diff --exit-code apps/www/lib/benchmark/benchmark.json test-vite-compatibility: runs-on: ubuntu-latest diff --git a/apps/www/app/(home)/aurora.css b/apps/www/app/(home)/aurora.css index a6b77f96a..46431d34c 100644 --- a/apps/www/app/(home)/aurora.css +++ b/apps/www/app/(home)/aurora.css @@ -1622,13 +1622,14 @@ width: 100%; } +/* Telemetry track & compound bar flex layout */ .home-aurora__telemetry-track { position: relative; display: flex; align-items: center; justify-content: space-between; width: 100%; - height: 1.85rem; + height: 2.125rem; padding: 0 0.85rem; border-radius: 0.375rem; background: color-mix(in oklch, var(--color-paper-3) 50%, transparent); @@ -1636,41 +1637,81 @@ overflow: hidden; } -.home-aurora__telemetry-bar { +/* Container holding the compound bar segments in a flex row */ +.home-aurora__telemetry-bar-group { position: absolute; left: 0; top: 0; bottom: 0; + display: flex; + align-items: stretch; border-radius: 0.3rem; - background: color-mix(in oklch, var(--color-muted) 30%, transparent); + overflow: hidden; transition: width 500ms var(--ease-out); + pointer-events: none; } -/* Telemetry color hierarchy: primary/secondary illuminated, competitors dark slate */ -.home-aurora__telemetry-row[data-tier="primary"] .home-aurora__telemetry-bar { - background: color-mix(in oklch, #3fb950 35%, transparent); - border-right: 2px solid #3fb950; - box-shadow: 0 0 10px color-mix(in oklch, #3fb950 35%, transparent); +.home-aurora__telemetry-segment { + height: 100%; + flex-shrink: 0; + pointer-events: auto; + transition: width 500ms var(--ease-out); } -.home-aurora__telemetry-row[data-tier="secondary"] .home-aurora__telemetry-bar { - background: color-mix(in oklch, #3fb950 18%, transparent); - border-right: 2px solid color-mix(in oklch, #3fb950 50%, transparent); - box-shadow: none; +/* Primary (ArkEnv) tier: vibrant green/cyan engine with soft tinted validator extension */ +.home-aurora__telemetry-row[data-tier="primary"] .home-aurora__telemetry-bar-group { + box-shadow: 0 0 12px color-mix(in oklch, #3fb950 25%, transparent); } -.home-aurora__telemetry-row[data-tier="competitor"] .home-aurora__telemetry-bar { - background: #2a2d32; - border-right: none; - box-shadow: none; +.home-aurora__telemetry-row[data-tier="primary"] .home-aurora__telemetry-segment--engine { + background: #3fb950; } -.home-aurora__telemetry-link { +.home-aurora__telemetry-row[data-tier="primary"] .home-aurora__telemetry-segment--validator { + background: color-mix(in oklch, #3fb950 32%, transparent); + border-inline-start: 1px solid color-mix(in oklch, #3fb950 65%, transparent); +} + +/* Competitor (T3 Env) tier: dark slate engine with matching extension */ +.home-aurora__telemetry-row[data-tier="competitor"] .home-aurora__telemetry-segment--engine { + background: #383e47; +} + +.home-aurora__telemetry-row[data-tier="competitor"] .home-aurora__telemetry-segment--validator { + background: #22252a; + border-inline-start: 1px solid #484f58; +} + +/* Reference (Varlock) tier: neutral grey standalone bar */ +.home-aurora__telemetry-row[data-tier="reference"] .home-aurora__telemetry-segment--engine { + background: #21262d; + border-right: 1px solid #30363d; +} + +/* Telemetry track foreground content */ +.home-aurora__telemetry-content { position: relative; z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + gap: 0.5rem; + pointer-events: none; +} + +.home-aurora__telemetry-meta { + display: inline-flex; + align-items: baseline; + gap: 0.375rem; + min-width: 0; + pointer-events: auto; +} + +.home-aurora__telemetry-link { text-decoration: none; display: inline-flex; - align-items: center; + align-items: baseline; color: inherit; cursor: pointer; } @@ -1679,6 +1720,7 @@ font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.8125rem; font-weight: 500; + line-height: normal; color: var(--color-ink); transition: color var(--dur-micro) var(--ease-out); } @@ -1689,17 +1731,132 @@ text-underline-offset: 2px; } +.home-aurora__telemetry-ext { + font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); + font-size: 0.8125rem; + font-weight: 500; + line-height: normal; + color: var(--color-ink-2); + white-space: nowrap; +} + +.home-aurora__telemetry-note { + font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); + font-size: 0.75rem; + line-height: normal; + color: var(--color-muted); + white-space: nowrap; +} + +.home-aurora__telemetry-stats { + display: inline-flex; + align-items: baseline; + gap: 0.35rem; + margin-left: auto; + white-space: nowrap; + pointer-events: auto; +} + .home-aurora__telemetry-size { - position: relative; - z-index: 1; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.8125rem; font-variant-numeric: tabular-nums; font-weight: 500; color: var(--color-ink); +} + +.home-aurora__telemetry-breakdown { + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.6875rem; + font-variant-numeric: tabular-nums; + color: var(--color-ink-2); + opacity: 0.8; +} + +/* Legend */ +.home-aurora__telemetry-legend { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.25rem 0.5rem 0.125rem; + font-size: 0.6875rem; + color: var(--color-ink-2); + flex-wrap: wrap; +} + +.home-aurora__telemetry-legend-item { + display: inline-flex; + align-items: center; + gap: 0.375rem; +} + +.home-aurora__telemetry-swatch { + width: 0.625rem; + height: 0.625rem; + border-radius: 0.125rem; + flex-shrink: 0; +} + +.home-aurora__telemetry-swatch--engine { + background: #3fb950; +} + +.home-aurora__telemetry-swatch--validator { + background: color-mix(in oklch, #3fb950 35%, transparent); + border: 1px solid color-mix(in oklch, #3fb950 65%, transparent); +} + +.home-aurora__telemetry-swatch--reference { + background: #21262d; + border: 1px solid #30363d; +} + +/* Telemetry section header: h2+subtitle on left, tabs on right */ +.home-aurora__telemetry-heading-row { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.home-aurora__telemetry-tabs-wrap { margin-left: auto; } +/* Benchmark receipt footnote */ +.home-aurora__telemetry-footer { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + padding: 0.5rem 0.85rem 0.65rem; + border-top: var(--rule-hair) solid var(--color-rule); + color: var(--color-ink-2); + font-size: 0.6875rem; + line-height: 1.4; +} + +.home-aurora__telemetry .home-aurora__telemetry-footer a { + color: var(--color-ink-2); + text-decoration: none; + transition: color var(--dur-micro) var(--ease-out); +} + +.home-aurora__telemetry .home-aurora__telemetry-footer a:hover { + color: var(--color-ink); + text-decoration: underline; + text-underline-offset: 2px; +} + +/* Metric subtitle under pitch heading */ +.home-aurora__telemetry-subtitle { + font-size: 0.75rem; + color: var(--color-ink-2); + line-height: 1.45; + margin-top: 0.125rem; +} + /* 05 · Automatic coercion — hub-and-spoke node graph animation */ /* 05 · Automatic coercion — Payload Pipeline */ .home-aurora__stream-widget { @@ -3049,8 +3206,8 @@ --color-fd-popover: var(--color-paper-2); --color-fd-popover-foreground: var(--color-ink); --color-fd-border: var(--color-rule); - /* Zod's 8-line window — Valibot nests PORT and scrolls inside this cap. */ - --mvp-lines: 8; + /* Window sized for Zod (8 lines) with comfortable breathing room — Valibot (16 lines) scrolls inside this cap. */ + --mvp-lines: 8.5; --mvp-lh: 1.55; --mvp-body-size: calc(var(--mvp-lines) * var(--mvp-lh) * var(--text-code) + 2 * var(--space-sm)); display: grid; diff --git a/apps/www/components/page/hero-mvp-example-view.tsx b/apps/www/components/page/hero-mvp-example-view.tsx index f85109cc9..05f825811 100644 --- a/apps/www/components/page/hero-mvp-example-view.tsx +++ b/apps/www/components/page/hero-mvp-example-view.tsx @@ -31,13 +31,13 @@ type ScrollEdgeFades = { }; function readScrollEdgeFades(scroll: HTMLElement): ScrollEdgeFades { - const canScroll = scroll.scrollHeight > scroll.clientHeight + 1; + const canScroll = scroll.scrollHeight - scroll.clientHeight > 6; if (!canScroll) { return { overflow: false, fadeTop: false, fadeBottom: false }; } - const atTop = scroll.scrollTop <= 1; + const atTop = scroll.scrollTop <= 2; const atBottom = - scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 1; + scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 2; return { overflow: true, fadeTop: !atTop, diff --git a/apps/www/components/page/hero-mvp-snippets.test.ts b/apps/www/components/page/hero-mvp-snippets.test.ts index 81e8332c6..45e38e38e 100644 --- a/apps/www/components/page/hero-mvp-snippets.test.ts +++ b/apps/www/components/page/hero-mvp-snippets.test.ts @@ -8,7 +8,7 @@ import { import { heroTwoslashOptions } from "./hero-mvp-twoslash-options"; describe("hero MVP snippets", () => { - it("typecheck in Twoslash", { timeout: 30_000 }, () => { + it("typecheck in Twoslash", { timeout: 60_000 }, () => { const slashers = { arktype: createTwoslasher(heroTwoslashOptions("arktype").twoslashOptions), standard: createTwoslasher( diff --git a/apps/www/components/page/runtime-bloat-showcase.test.tsx b/apps/www/components/page/runtime-bloat-showcase.test.tsx index 4321ec9cd..2e42730ca 100644 --- a/apps/www/components/page/runtime-bloat-showcase.test.tsx +++ b/apps/www/components/page/runtime-bloat-showcase.test.tsx @@ -1,47 +1,175 @@ import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it } from "vitest"; import { RuntimeBloatShowcase } from "./runtime-bloat-showcase"; describe("RuntimeBloatShowcase", () => { - it("renders the Optimized for the edge heading and comparison copy", () => { + // Prevent window.location.search leaking between tests via replaceState. + beforeEach(() => { + window.history.replaceState(null, "", "/"); + }); + + it("renders the heading and metric subtitle", () => { render(); expect( screen.getByRole("heading", { name: "Optimized for the edge" }), ).toBeInTheDocument(); - expect( - screen.getByText(/50% smaller core than T3 Env/i), - ).toBeInTheDocument(); - expect( - screen.getByText(/All engines under 10 kB for strict edge deployments/i), - ).toBeInTheDocument(); + expect(screen.getByText(/Minified, uncompressed JS/i)).toBeInTheDocument(); + }); + + it("renders the 3 validator tabs with ArkType selected by default", () => { + render(); + + const tablist = screen.getByRole("tablist", { + name: /Validator comparison/i, + }); + expect(tablist).toBeInTheDocument(); + + const arkTypeTab = screen.getByRole("tab", { name: /ArkType/i }); + const zodTab = screen.getByRole("tab", { name: /Zod/i }); + const valibotTab = screen.getByRole("tab", { name: /Valibot/i }); + + expect(arkTypeTab).toBeInTheDocument(); + expect(zodTab).toBeInTheDocument(); + expect(valibotTab).toBeInTheDocument(); + + expect(arkTypeTab).toHaveAttribute("aria-selected", "true"); + expect(zodTab).toHaveAttribute("aria-selected", "false"); + expect(valibotTab).toHaveAttribute("aria-selected", "false"); + }); + + it("renders the compound bar legend", () => { + render(); + + expect(screen.getByText("Engine")).toBeInTheDocument(); + expect(screen.getByText("Validator extension")).toBeInTheDocument(); + expect(screen.getByText("All-in-one (for reference)")).toBeInTheDocument(); + }); + + it("defaults to ArkType tab sorted by engine size with compound bars", () => { + render(); const figure = screen.getByRole("figure", { name: /production runtime bundle size comparison/i, }); - expect(figure).toHaveTextContent("@arkenv/standard"); - expect(figure).toHaveTextContent("1.5 kB"); + + // ArkEnv core bar expect(figure).toHaveTextContent("@arkenv/core"); - expect(figure).toHaveTextContent("7.4 kB"); + expect(figure).toHaveTextContent("+ ArkType"); + expect(figure).toHaveTextContent("156.0 kB"); + expect(figure).toHaveTextContent("(6.3 + 149.8)"); + + // T3 Env bar with Zod requirement note expect(figure).toHaveTextContent("@t3-oss/env-core"); - expect(figure).toHaveTextContent("14.2 kB"); + expect(figure).toHaveTextContent("+ Zod"); + expect(figure).toHaveTextContent("(requires Zod)"); + expect(figure).toHaveTextContent("325.0 kB"); + + // Varlock reference bar expect(figure).toHaveTextContent("varlock"); + expect(figure).toHaveTextContent("(for reference)"); expect(figure).toHaveTextContent("28.4 kB"); + }); + + it("switches to Valibot tab, updates top bar to @arkenv/standard, and syncs URL", async () => { + render(); + + const valibotTab = screen.getByRole("tab", { name: /Valibot/i }); + await userEvent.click(valibotTab); + + expect(window.location.search).toBe("?validator=valibot"); + + const figure = screen.getByRole("figure", { + name: /production runtime bundle size comparison/i, + }); + + // Shows @arkenv/standard with 23.3 kB total + expect(figure).toHaveTextContent("@arkenv/standard"); + expect(figure).toHaveTextContent("+ Valibot"); + expect(figure).toHaveTextContent("23.3 kB"); + + // T3 Env remains 325.0 kB with (requires Zod) note + expect(figure).toHaveTextContent("@t3-oss/env-core"); + expect(figure).toHaveTextContent("(requires Zod)"); + expect(figure).toHaveTextContent("325.0 kB"); + }); + + it("switches to Zod tab and back to ArkType clearing query string", async () => { + render(); + + const zodTab = screen.getByRole("tab", { name: /Zod/i }); + await userEvent.click(zodTab); + + expect(window.location.search).toBe("?validator=zod"); + + const figure = screen.getByRole("figure", { + name: /production runtime bundle size comparison/i, + }); + expect(figure).toHaveTextContent("@arkenv/standard"); + expect(figure).toHaveTextContent("+ Zod"); + expect(figure).toHaveTextContent("329.2 kB"); + + expect(figure).toHaveTextContent("@t3-oss/env-core"); + expect(figure).toHaveTextContent("325.0 kB"); + + // Switching back to default ArkType clears ?validator param + const arkTypeTab = screen.getByRole("tab", { name: /ArkType/i }); + await userEvent.click(arkTypeTab); + + expect(window.location.search).toBe(""); + expect(figure).toHaveTextContent("@arkenv/core"); + }); + + it("initializes to Valibot view when ?validator=valibot is in URL", () => { + window.history.replaceState(null, "", "/?validator=valibot"); + render(); + + const figure = screen.getByRole("figure", { + name: /production runtime bundle size comparison/i, + }); + expect(figure).toHaveTextContent("@arkenv/standard"); + expect(figure).toHaveTextContent("+ Valibot"); + expect(figure).toHaveTextContent("23.3 kB"); + }); + + it("renders accessible image labels on compound bar tracks", () => { + render(); + + const images = screen.getAllByRole("img"); + expect( + images.some((img) => + img + .getAttribute("aria-label") + ?.includes("@arkenv/core engine at 6.3 kilobytes"), + ), + ).toBe(true); + }); + + it("renders npmx links and benchmark receipts link", () => { + render(); - // Check npmx links const links = screen.getAllByRole("link"); expect( links.some( - (l) => l.getAttribute("href") === "https://npmx.dev/package/varlock", + (l) => + l.getAttribute("href") === "https://npmx.dev/package/@arkenv/core", ), ).toBe(true); expect( links.some( - (l) => - l.getAttribute("href") === - "https://npmx.dev/package/@arkenv/standard", + (l) => l.getAttribute("href") === "https://npmx.dev/package/varlock", ), ).toBe(true); + + const receipts = screen.getByRole("link", { + name: /View benchmark script/i, + }); + expect(receipts).toBeInTheDocument(); + expect(receipts).toHaveAttribute( + "href", + "https://github.com/yamcodes/arkenv/blob/v1/scripts/benchmark-bundle-size.ts", + ); }); }); diff --git a/apps/www/components/page/runtime-bloat-showcase.tsx b/apps/www/components/page/runtime-bloat-showcase.tsx index 52fde8175..5654ae0c5 100644 --- a/apps/www/components/page/runtime-bloat-showcase.tsx +++ b/apps/www/components/page/runtime-bloat-showcase.tsx @@ -1,35 +1,61 @@ -const BUNDLE_DATA = [ - { - name: "@arkenv/standard", - size: "1.5 kB", - width: "8%", - tier: "primary", - }, - { - name: "@arkenv/core", - size: "7.4 kB", - width: "28%", - tier: "secondary", - }, - { - name: "@t3-oss/env-core", - size: "14.2 kB", - width: "52%", - tier: "competitor", - }, - { - name: "varlock", - size: "28.4 kB", - width: "100%", - tier: "competitor", - }, -]; +"use client"; + +import { useEffect, useState } from "react"; +import benchmarkDataRaw from "~/lib/benchmark/benchmark.json"; +import type { BenchmarkData, ValidatorTab } from "~/lib/benchmark/types"; +import { ValidatorMark } from "./hero-mvp-marks"; +import { InkTabList } from "./ink-tabs"; + +const benchmarkData = benchmarkDataRaw as BenchmarkData; + +const VALIDATOR_TABS: readonly { id: ValidatorTab; label: string }[] = [ + { id: "arktype", label: "ArkType" }, + { id: "zod", label: "Zod" }, + { id: "valibot", label: "Valibot" }, +] as const; /** - * Performance & zero-runtime-bloat showcase with highlighted ArkEnv tiers - * and muted dark neutral competitor bars. + * "Optimized for the edge" bento cell: pkg-size-style compound bar chart comparing + * engine footprint and validator extensions across ArkType, Zod, and Valibot. + * + * Bars are strictly sorted by engine size: + * 1. ArkEnv (@arkenv/core or @arkenv/standard) + * 2. T3 Env (@t3-oss/env-core) + * 3. Varlock (all-in-one engine + validator, for reference) */ export function RuntimeBloatShowcase() { + const [validator, setValidator] = useState("arktype"); + + // Client-safe URL sync: defaults to "arktype" on server to avoid hydration mismatch + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const raw = params.get("validator"); + if (raw === "zod" || raw === "valibot") { + setValidator(raw); + } + }, []); + + function switchValidator(next: ValidatorTab) { + setValidator(next); + const url = new URL(window.location.href); + if (next === "arktype") { + url.searchParams.delete("validator"); + } else { + url.searchParams.set("validator", next); + } + window.history.replaceState(null, "", url.toString()); + } + + const rows = benchmarkData[validator]; + // Globally consistent scale across all tabs (~337 kB max from T3 Env / Zod) + const maxBytes = Math.max( + ...[ + benchmarkData.arktype, + benchmarkData.zod, + benchmarkData.valibot, + ].flatMap((list) => list.map((r) => r.totalBytes)), + ); + return (
-

- Optimized for the edge -

-

- 50% smaller core than T3 Env. All engines under 10 kB for strict edge - deployments. -

+
+
+

+ Optimized for the edge +

+

+ Minified, uncompressed JS evaluated during V8 isolate cold starts. +

+
+ +
+ ({ + id: tab.id, + label: ( + <> + + {tab.label} + + ), + }))} + /> +
+
-
- {BUNDLE_DATA.map((item) => ( -
-
+ + +
+ {rows.map((item) => { + const totalPct = `${((item.totalBytes / maxBytes) * 100).toFixed(1)}%`; + const engineShare = `${((item.engineBytes / item.totalBytes) * 100).toFixed(1)}%`; + const validatorShare = item.validatorBytes + ? `${((item.validatorBytes / item.totalBytes) * 100).toFixed(1)}%` + : "0%"; + + const a11yLabel = `${item.name} engine at ${item.engineKb} kilobytes${ + item.validatorName + ? `, plus ${item.validatorName} extension at ${item.validatorKb} kilobytes` + : "" + }, total ${item.totalKb} kilobytes`; + + return ( +
- ))} + ); + })} +
+ +
+ ArkEnv built via esbuild · platform: neutral · target: es2022{" "} + {" "} + + View benchmark script ↗ +
diff --git a/apps/www/content/docs/core-concepts/hosting-presets.mdx b/apps/www/content/docs/core-concepts/hosting-presets.mdx index 1192176fa..fb989d2d9 100644 --- a/apps/www/content/docs/core-concepts/hosting-presets.mdx +++ b/apps/www/content/docs/core-concepts/hosting-presets.mdx @@ -454,5 +454,5 @@ Every preset field is **optional** (present only when deployed on that provider) - + diff --git a/apps/www/content/docs/guides/migrating-from-t3-env.mdx b/apps/www/content/docs/guides/migrating-from-t3-env.mdx index 0ad9539dd..56d2cb58c 100644 --- a/apps/www/content/docs/guides/migrating-from-t3-env.mdx +++ b/apps/www/content/docs/guides/migrating-from-t3-env.mdx @@ -108,6 +108,24 @@ Work through these steps on an existing Next.js app. }); ``` + ArkEnv (Zod Mini — 90% smaller edge bundle): + + ```ts title="./env.ts" twoslash + import { arkenv } from "@arkenv/standard/zod-mini"; + import * as z from "zod/mini"; + + export const env = arkenv({ + DATABASE_URL: z.url(), + NEXT_PUBLIC_APP_URL: z.url(), + }); + ``` + + + T3 Env with classic Zod ships a \~325 kB uncompressed validation payload to your edge functions. + Switching to `@arkenv/standard/zod-mini` and `zod/mini` preserves your familiar Zod syntax + while dropping the total edge validation footprint to **33.6 kB** — a \~90% reduction in V8 cold-start parse cost. + + You do not copy keys into `runtimeEnv`. `NEXT_PUBLIC_` marks client variables. diff --git a/apps/www/content/docs/validators/choosing-a-validator.mdx b/apps/www/content/docs/validators/choosing-a-validator.mdx new file mode 100644 index 000000000..d68d1ace8 --- /dev/null +++ b/apps/www/content/docs/validators/choosing-a-validator.mdx @@ -0,0 +1,158 @@ +--- +title: Choosing a validator +description: Compare bundle size, syntax, and runtime characteristics across ArkType, Valibot, Zod Mini, and classic Zod. +--- + +ArkEnv supports multiple validation libraries through a unified environment variables pipeline. +ArkEnv lets you pick the schema engine that matches your performance requirements and coding style. + +Both engines (`@arkenv/core` and `@arkenv/standard`) run the same coercion pipeline, +produce the same fail-fast errors, and support all framework plugins (Next.js, Nuxt, Vite, Bun). +The difference lies in bundle weight, syntax, and ecosystem integration. + +## Comparison matrix + +All figures represent minified JavaScript evaluated during V8 isolate cold starts, +measured using esbuild (`platform: neutral`, `target: es2022`). Both uncompressed bytes and gzip footprints are measured and generated by `scripts/benchmark-bundle-size.ts`. + +| Validator | Engine / Subpath | Total edge payload | Min + gzip | Primary strength | +| :-------------- | :-------------------------- | :----------------- | :--------- | :------------------------------------------------------------------ | +| **Valibot** | `@arkenv/standard/valibot` | **23.3 kB** | 7.5 kB | Smallest edge footprint; modular functional tree-shaking | +| **Zod Mini** | `@arkenv/standard/zod-mini` | **33.6 kB** | 11.5 kB | \~90% smaller than classic Zod; familiar syntax | +| **ArkType** | `@arkenv/core` | **156.0 kB** | 48.5 kB | TypeScript-native DSL strings; built-in keywords; zero dependencies | +| **Classic Zod** | `@arkenv/standard` | **329.2 kB** | 67.1 kB | Drop-in compatibility for existing Zod schemas (Zod 4) | + +## Strict edge constraints: Valibot + +Choose **Valibot** via `@arkenv/standard/valibot` when minimizing cold-start latency on edge runtimes +(Cloudflare Workers, Vercel Edge Functions, AWS Lambda\@Edge) is your top priority. + +Valibot uses standalone functions rather than monolithic class instances or chaining methods. +Unused schema types are completely stripped by your bundler: the treeshaken validator weighs just +**2.4 kB**, bringing the total runtime validation payload down to **23.3 kB**. + +```package-install +npm install @arkenv/standard valibot +``` + +```ts title="./env.ts" twoslash +import { arkenv } from "@arkenv/standard/valibot"; +import * as v from "valibot"; + +export const env = arkenv({ + DATABASE_URL: v.pipe(v.string(), v.url()), + PORT: v.optional(v.number(), 3000), + NODE_ENV: v.optional( + v.picklist(["development", "production", "test"]), + "development", + ), +}); +``` + +See the [Valibot guide](/docs/validators/valibot) for detailed schema recipes and pipe usage. + +## Seamless migrations with massive savings: Zod Mini + +Choose **Zod Mini** via `@arkenv/standard/zod-mini` when migrating an existing Zod codebase +where you want to retain familiar `z.string()`, `z.number()`, and `z.boolean()` APIs without +shipping Zod's \~320 kB runtime bundle to the edge. + +Zod 4's `zod/mini` subpath provides modular, tree-shakeable schema constructors. Paired with +ArkEnv's pre-bound adapter, the entire payload evaluates at **33.6 kB** — shedding **\~90%** of +classic Zod's parsing overhead. + +```package-install +npm install @arkenv/standard zod +``` + +```ts title="./env.ts" twoslash +import { arkenv } from "@arkenv/standard/zod-mini"; +import * as z from "zod/mini"; + +export const env = arkenv({ + DATABASE_URL: z.url(), + PORT: z.number(), + DEBUG: z.boolean(), +}); +``` + + + `@arkenv/standard/zod-mini` automatically binds `zod/mini`'s JSON Schema converter + so string environment variables like `"3000"` coerce to numbers and `"true"` to booleans + without needing manual `z.coerce` wrappers. + + +See the [Zod guide](/docs/validators/zod#zod-mini) for subpath configuration. + +## Advanced TypeScript integration: ArkType + +Choose **ArkType** via `@arkenv/core` when you want an expressive, TypeScript-first developer +experience with zero runtime dependencies. + +ArkType defines validation schemas directly using TypeScript syntax within definition strings. +It compiles definitions at runtime into optimized validation functions while offering 100x faster +IDE autocomplete and typecheck cycles than generic type builders. ArkEnv also provides built-in +domain keywords like `number.port` and `string.url`. + +At **156.0 kB**, `@arkenv/core` with ArkType evaluates in less than half the bytes of classic +Zod while providing a full runtime type engine. + +```package-install +npm install @arkenv/core arktype +``` + +```ts title="./env.ts" twoslash +import arkenv from "@arkenv/core"; + +export const env = arkenv({ + DATABASE_URL: "string.url", + PORT: "number.port = 3000", + DEBUG: "boolean = false", + NODE_ENV: "'development' | 'production' | 'test' = 'development'", +}); +``` + +See the [ArkType guide](/docs/validators/arktype) for keywords, cyclic types, and custom regex definitions. + +## Existing monolithic schemas: classic Zod + +Choose **classic Zod** via `@arkenv/standard` when you already have extensive Zod 4 schemas +or want zero code changes during migration, and your target runtime is a long-lived Node.js server +where isolate cold-start parse time is not a constraint. + +```package-install +npm install @arkenv/standard zod +``` + +```ts title="./env.ts" twoslash +import arkenv from "@arkenv/standard"; +import * as z from "zod"; + +export const env = arkenv({ + DATABASE_URL: z.url(), + PORT: z.int().min(0).max(65535).default(3000), + NODE_ENV: z + .enum(["development", "production", "test"]) + .default("development"), +}); +``` + +Zod 4.2+ embeds Standard JSON Schema metadata, enabling automatic string-to-type coercion +without manual `toJsonSchema` callbacks or `z.coerce`. + +Zod 4's default export includes its complete schema library, evaluating at **329.2 kB** uncompressed +(**67.1 kB** gzip). For long-lived Node.js servers, cold-start parse size is negligible. When targeting +serverless or edge functions, switch to [Zod Mini](#seamless-migrations-with-massive-savings-zod-mini) +(**33.6 kB**) or [Valibot](#strict-edge-constraints-valibot) (**23.3 kB**) to minimize cold-start latency. + +## Next steps + + + + + + + + + + diff --git a/apps/www/content/docs/validators/index.mdx b/apps/www/content/docs/validators/index.mdx index e125adcc9..e899201af 100644 --- a/apps/www/content/docs/validators/index.mdx +++ b/apps/www/content/docs/validators/index.mdx @@ -123,9 +123,14 @@ framework plugins that keep server secrets out of the client graph. See [Why ArkEnv?](/docs/why-arkenv) for DIY, Varlock, T3 Env, znv, Envalid, and related tools. +For a detailed breakdown of bundle sizes and cold-start parse payloads (Valibot 23.3 kB vs Zod Mini 33.6 kB vs ArkType 156.0 kB), +see [Choosing a validator](/docs/validators/choosing-a-validator). + ## Cookbooks + + diff --git a/apps/www/content/docs/validators/meta.json b/apps/www/content/docs/validators/meta.json index e6e1d7961..cfe02e230 100644 --- a/apps/www/content/docs/validators/meta.json +++ b/apps/www/content/docs/validators/meta.json @@ -1,4 +1,4 @@ { "title": "Validators", - "pages": ["index", "arktype", "zod", "valibot"] + "pages": ["index", "choosing-a-validator", "arktype", "zod", "valibot"] } diff --git a/apps/www/content/docs/validators/zod.mdx b/apps/www/content/docs/validators/zod.mdx index b0b718f49..f9160af38 100644 --- a/apps/www/content/docs/validators/zod.mdx +++ b/apps/www/content/docs/validators/zod.mdx @@ -77,6 +77,11 @@ TypeScript must use `moduleResolution: "bundler" | "node16" | "nodenext"`. The root `toJsonSchema` callback remains available for mixed maps; see [Coercion](/docs/core-concepts/coercion-and-parsing#mixing-with-zod-mini). +Zod Mini gives you the modular tree-shaking benefits of Zod 4: paired with +`@arkenv/standard/zod-mini`, the combined validation payload evaluates at just +**33.6 kB** uncompressed JavaScript (\~11.5 kB gzip), compared to \~329 kB for +classic monolithic Zod. See [Choosing a validator](/docs/validators/choosing-a-validator). + ## Zod v3 Zod v3 (including `zod/v3` subpath exports in Zod 4) implements Standard Schema diff --git a/apps/www/lib/benchmark/benchmark.json b/apps/www/lib/benchmark/benchmark.json new file mode 100644 index 000000000..72bd50661 --- /dev/null +++ b/apps/www/lib/benchmark/benchmark.json @@ -0,0 +1,207 @@ +{ + "arktype": [ + { + "id": "arkenv-core", + "name": "@arkenv/core", + "npmPackage": "@arkenv/core", + "engineBytes": 6424, + "engineKb": "6.3", + "engineGzipBytes": 2913, + "engineGzipKb": "2.8", + "validatorName": "ArkType", + "validatorBytes": 153347, + "validatorKb": "149.8", + "validatorGzipBytes": 46718, + "validatorGzipKb": "45.6", + "totalBytes": 159771, + "totalKb": "156.0", + "totalGzipBytes": 49631, + "totalGzipKb": "48.5", + "tier": "primary" + }, + { + "id": "t3-env", + "name": "@t3-oss/env-core", + "npmPackage": "@t3-oss/env-core", + "engineBytes": 14541, + "engineKb": "14.2", + "engineGzipBytes": 4300, + "engineGzipKb": "4.2", + "validatorName": "Zod", + "validatorBytes": 318259, + "validatorKb": "310.8", + "validatorGzipBytes": 63284, + "validatorGzipKb": "61.8", + "totalBytes": 332800, + "totalKb": "325.0", + "totalGzipBytes": 67584, + "totalGzipKb": "66.0", + "tier": "competitor", + "note": "requires Zod" + }, + { + "id": "varlock", + "name": "varlock", + "npmPackage": "varlock", + "engineBytes": 29082, + "engineKb": "28.4", + "engineGzipBytes": 9318, + "engineGzipKb": "9.1", + "totalBytes": 29082, + "totalKb": "28.4", + "totalGzipBytes": 9318, + "totalGzipKb": "9.1", + "tier": "reference", + "note": "for reference" + } + ], + "zod": [ + { + "id": "arkenv-standard", + "name": "@arkenv/standard", + "npmPackage": "@arkenv/standard", + "engineBytes": 10222, + "engineKb": "10.0", + "engineGzipBytes": 4106, + "engineGzipKb": "4.0", + "validatorName": "Zod", + "validatorBytes": 326923, + "validatorKb": "319.3", + "validatorGzipBytes": 64576, + "validatorGzipKb": "63.1", + "totalBytes": 337145, + "totalKb": "329.2", + "totalGzipBytes": 68682, + "totalGzipKb": "67.1", + "tier": "primary" + }, + { + "id": "t3-env", + "name": "@t3-oss/env-core", + "npmPackage": "@t3-oss/env-core", + "engineBytes": 14541, + "engineKb": "14.2", + "engineGzipBytes": 4300, + "engineGzipKb": "4.2", + "validatorName": "Zod", + "validatorBytes": 318259, + "validatorKb": "310.8", + "validatorGzipBytes": 63284, + "validatorGzipKb": "61.8", + "totalBytes": 332800, + "totalKb": "325.0", + "totalGzipBytes": 67584, + "totalGzipKb": "66.0", + "tier": "competitor" + }, + { + "id": "varlock", + "name": "varlock", + "npmPackage": "varlock", + "engineBytes": 29082, + "engineKb": "28.4", + "engineGzipBytes": 9318, + "engineGzipKb": "9.1", + "totalBytes": 29082, + "totalKb": "28.4", + "totalGzipBytes": 9318, + "totalGzipKb": "9.1", + "tier": "reference", + "note": "for reference" + } + ], + "valibot": [ + { + "id": "arkenv-standard", + "name": "@arkenv/standard", + "npmPackage": "@arkenv/standard", + "engineBytes": 10222, + "engineKb": "10.0", + "engineGzipBytes": 4106, + "engineGzipKb": "4.0", + "validatorName": "Valibot", + "validatorBytes": 13675, + "validatorKb": "13.4", + "validatorGzipBytes": 3612, + "validatorGzipKb": "3.5", + "totalBytes": 23897, + "totalKb": "23.3", + "totalGzipBytes": 7718, + "totalGzipKb": "7.5", + "tier": "primary" + }, + { + "id": "t3-env", + "name": "@t3-oss/env-core", + "npmPackage": "@t3-oss/env-core", + "engineBytes": 14541, + "engineKb": "14.2", + "engineGzipBytes": 4300, + "engineGzipKb": "4.2", + "validatorName": "Zod", + "validatorBytes": 318259, + "validatorKb": "310.8", + "validatorGzipBytes": 63284, + "validatorGzipKb": "61.8", + "totalBytes": 332800, + "totalKb": "325.0", + "totalGzipBytes": 67584, + "totalGzipKb": "66.0", + "tier": "competitor", + "note": "requires Zod" + }, + { + "id": "varlock", + "name": "varlock", + "npmPackage": "varlock", + "engineBytes": 29082, + "engineKb": "28.4", + "engineGzipBytes": 9318, + "engineGzipKb": "9.1", + "totalBytes": 29082, + "totalKb": "28.4", + "totalGzipBytes": 9318, + "totalGzipKb": "9.1", + "tier": "reference", + "note": "for reference" + } + ], + "matrix": { + "valibot": { + "engine": "@arkenv/standard/valibot", + "subpath": "@arkenv/standard/valibot", + "totalBytes": 23897, + "totalKb": "23.3", + "gzipBytes": 7718, + "gzipKb": "7.5", + "description": "Smallest edge footprint; modular functional tree-shaking" + }, + "zodMini": { + "engine": "@arkenv/standard/zod-mini", + "subpath": "@arkenv/standard/zod-mini", + "totalBytes": 34456, + "totalKb": "33.6", + "gzipBytes": 11735, + "gzipKb": "11.5", + "description": "~90% smaller than classic Zod; familiar syntax" + }, + "arktype": { + "engine": "@arkenv/core", + "subpath": "@arkenv/core", + "totalBytes": 159771, + "totalKb": "156.0", + "gzipBytes": 49631, + "gzipKb": "48.5", + "description": "TypeScript-native DSL strings; built-in keywords; zero dependencies" + }, + "classicZod": { + "engine": "@arkenv/standard", + "subpath": "@arkenv/standard", + "totalBytes": 337145, + "totalKb": "329.2", + "gzipBytes": 68682, + "gzipKb": "67.1", + "description": "Drop-in compatibility for existing Zod schemas (Zod 4)" + } + } +} diff --git a/apps/www/lib/benchmark/types.ts b/apps/www/lib/benchmark/types.ts new file mode 100644 index 000000000..15078b4e4 --- /dev/null +++ b/apps/www/lib/benchmark/types.ts @@ -0,0 +1,46 @@ +export type BenchmarkTier = "primary" | "competitor" | "reference"; + +export type BenchmarkRow = { + id: string; + name: string; + npmPackage: string; + engineBytes: number; + engineKb: string; + engineGzipBytes: number; + engineGzipKb: string; + validatorName?: string; + validatorBytes?: number; + validatorKb?: string; + validatorGzipBytes?: number; + validatorGzipKb?: string; + totalBytes: number; + totalKb: string; + totalGzipBytes: number; + totalGzipKb: string; + tier: BenchmarkTier; + note?: string; +}; + +export type BenchmarkMatrixItem = { + engine: string; + subpath: string; + totalBytes: number; + totalKb: string; + gzipBytes: number; + gzipKb: string; + description: string; +}; + +export type ValidatorTab = "arktype" | "zod" | "valibot"; + +export type BenchmarkData = { + arktype: BenchmarkRow[]; + zod: BenchmarkRow[]; + valibot: BenchmarkRow[]; + matrix: { + valibot: BenchmarkMatrixItem; + zodMini: BenchmarkMatrixItem; + arktype: BenchmarkMatrixItem; + classicZod: BenchmarkMatrixItem; + }; +}; diff --git a/apps/www/lib/twoslash-options.test.ts b/apps/www/lib/twoslash-options.test.ts index 0a1f25577..9af29c953 100644 --- a/apps/www/lib/twoslash-options.test.ts +++ b/apps/www/lib/twoslash-options.test.ts @@ -2,7 +2,7 @@ import { twoslasher } from "twoslash"; import { describe, expect, it } from "vitest"; import { arktypeTwoslashOptions } from "./twoslash-options"; -describe("arktypeTwoslashOptions", () => { +describe("arktypeTwoslashOptions", { timeout: 30000 }, () => { it("infers @arkenv/nextjs client variables as strings in docs snippets", () => { const result = twoslasher( `// @filename: env.ts @@ -68,7 +68,7 @@ const apiUrl = env.NEXT_PUBLIC_API_URL; }); it("resolves flat @arkenv/nextjs env without TS2307 errors", { - timeout: 15_000, + timeout: 45_000, }, () => { const resultNextjs = twoslasher( `// @errors: 2339 @@ -95,7 +95,7 @@ const db = env.DATABASE_URL; }); it("resolves flat @arkenv/nuxt env without TS2307 errors", { - timeout: 15_000, + timeout: 45_000, }, () => { const resultNuxt = twoslasher( `// @errors: 2339 diff --git a/apps/www/package.json b/apps/www/package.json index 690e66c9d..9fb3b2c78 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "mdx": "node ./bin/mdx", - "prebuild": "pnpm -w build --filter @arkenv/fumadocs-ui && pnpm run mdx", + "prebuild": "pnpm -w build --filter @arkenv/fumadocs-ui --filter @arkenv/standard --filter @arkenv/core && pnpm run mdx && tsx ../../scripts/benchmark-bundle-size.ts", "predev": "pnpm -w build --filter @arkenv/fumadocs-ui && pnpm run mdx", "build": "node ./bin/build next build", "dev": "next dev", diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index e3bd6a1b4..01e76db57 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -305,7 +305,7 @@ pnpm run test:e2e # E2E tests - Run `pnpm release` after merging PRs to publish packages - Only packages in `packages/` are published to npm -## Design Decisions +## Design decisions **Split Parsing Engines (ArkType vs Standard Schema):** @@ -470,7 +470,7 @@ A non-interactive muted label that only **groups** sibling **Leaves** under a ** - Visual direction is **hybrid**: Turbo structure/motion/active-pill; ArkEnv color tokens - Sidebar Install banner removed; use **Separators** for flat reference groupings and **Nested Folders** only when the URL is truly nested -### Environment Variable Validation +### Environment variable validation - ArkEnv uses ArkType's type system to validate environment variables - Schema is defined using TypeScript-like syntax (e.g., `"string.host"`, `"number.port"`) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6204ec73e..cce64b634 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -83,7 +83,7 @@ We **do not use Conventional Commits** (`feat:`, `fix:`, `chore:`, etc.). Instea ❌ Add Support For Custom Error Messages ``` -## Branching & Release Workflow +## Branching & release workflow We use a **Dual-Branch Model** (`dev` and `main`) to ensure the production documentation site is strictly synchronized with npm releases, meaning it never displays unreleased features. For the architectural reasoning behind this decision, see [ADR 0006: Branching and Release Flow](./adr/0006-branching-and-release-flow.md). @@ -102,9 +102,9 @@ We use a **Dual-Branch Model** (`dev` and `main`) to ensure the production docum └───────────┘ ``` -### Key Workflows +### Key workflows -#### Use Case 1: Developing a New Feature +#### Use case 1: Developing a new feature When adding functionality or new documentation pages for unreleased code: @@ -113,7 +113,7 @@ When adding functionality or new documentation pages for unreleased code: 3. Open a Pull Request targeting `dev`. 4. Merging to `dev` will deploy a Vercel Preview (for review), but it will **not** affect the production documentation site. -#### Use Case 2: Releasing Packages to npm +#### Use case 2: Releasing packages to npm When you are ready to publish the unreleased features currently sitting on `dev`: @@ -123,7 +123,7 @@ When you are ready to publish the unreleased features currently sitting on `dev` 4. A GitHub workflow will automatically build and publish the packages to npm. 5. Immediately after a successful publish, the workflow automatically fast-forwards the `main` branch to match `dev`. This push to `main` triggers the production documentation deploy. -#### Use Case 3: Fixing a Typo on the Live Docs +#### Use case 3: Fixing a typo on the live docs When you need to fix a typo or make a cosmetic change to the live documentation *without* publishing a new npm package: @@ -137,7 +137,7 @@ When you need to fix a typo or make a cosmetic change to the live documentation ``` This ensures the fix hits `main` instantly while preventing Git history drift. -#### Use Case 4: Coordinating a Major Version (e.g., v1) +#### Use case 4: Coordinating a major version (e.g., v1) When working on a massive marketing push, docs facelift, or breaking API changes that will take weeks or months to coordinate: diff --git a/docs/TESTING.md b/docs/TESTING.md index 1bf1dcfc3..73ec77b07 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -284,7 +284,7 @@ Examples are kept clean and focused on demonstrating usage: - `examples/basic` - Basic Node.js usage - `examples/with-bun` - Bun runtime usage -## Ci integration +## CI integration The CI pipeline runs: diff --git a/scripts/benchmark-bundle-size.ts b/scripts/benchmark-bundle-size.ts new file mode 100644 index 000000000..82c08d84c --- /dev/null +++ b/scripts/benchmark-bundle-size.ts @@ -0,0 +1,363 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { gzipSync } from "node:zlib"; +import { build } from "esbuild"; +import type { BenchmarkData } from "../apps/www/lib/benchmark/types"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, ".."); +const OUT_PATH = join(ROOT, "apps/www/lib/benchmark/benchmark.json"); + +type BuildTarget = { + code: string; + dir: string; + external?: string[]; + fallbackBytes: number; + fallbackGzipBytes: number; +}; + +async function measure( + target: BuildTarget, +): Promise<{ bytes: number; gzipBytes: number }> { + try { + const res = await build({ + stdin: { contents: target.code, resolveDir: target.dir }, + bundle: true, + minify: true, + treeShaking: true, + format: "esm", + platform: "neutral", + target: "es2022", + write: false, + external: target.external ?? [], + }); + const buffer = res.outputFiles[0].contents; + return { + bytes: buffer.length, + gzipBytes: gzipSync(buffer).length, + }; + } catch (err: unknown) { + console.warn( + `Falling back to static metrics for build target: ${err instanceof Error ? err.message : String(err)}`, + ); + return { + bytes: target.fallbackBytes, + gzipBytes: target.fallbackGzipBytes, + }; + } +} + +function toKb(bytes: number): string { + return (bytes / 1024).toFixed(1); +} + +async function run() { + // 1. Measure Core Engine & Core + ArkType + const coreEngine = await measure({ + code: `import arkenv from "${join(ROOT, "packages/core/dist/index.mjs")}"; console.log(arkenv);`, + dir: join(ROOT, "packages/core"), + external: ["arktype", "@ark/util", "@ark/schema", "arkregex"], + fallbackBytes: 6424, + fallbackGzipBytes: 2913, + }); + + const coreArkType = await measure({ + code: `import arkenv from "${join(ROOT, "packages/core/dist/index.mjs")}"; import { type } from "arktype"; console.log(arkenv(type({ PORT: "0 <= number.integer <= 65535" })));`, + dir: join(ROOT, "packages/core"), + fallbackBytes: 159771, + fallbackGzipBytes: 49631, + }); + + // 2. Measure Standard Engine & Standard + Valibot / Zod + const standardEngine = await measure({ + code: `import arkenv from "${join(ROOT, "packages/standard/dist/index.js")}"; console.log(arkenv);`, + dir: join(ROOT, "packages/standard"), + fallbackBytes: 10222, + fallbackGzipBytes: 4106, + }); + + const standardValibot = await measure({ + code: `import arkenv from "${join(ROOT, "packages/standard/dist/valibot.js")}"; import * as v from "valibot"; console.log(arkenv(v.object({ PORT: v.string() })));`, + dir: join(ROOT, "packages/standard"), + fallbackBytes: 23897, + fallbackGzipBytes: 7718, + }); + + const standardZod = await measure({ + code: `import arkenv from "${join(ROOT, "packages/standard/dist/index.js")}"; import { z } from "zod"; console.log(arkenv({ PORT: z.string() }));`, + dir: join(ROOT, "packages/standard"), + fallbackBytes: 337145, + fallbackGzipBytes: 68682, + }); + + const standardZodMini = await measure({ + code: `import arkenv from "${join(ROOT, "packages/standard/dist/zod-mini.js")}"; import * as z from "zod/mini"; console.log(arkenv(z.object({ PORT: z.string() })));`, + dir: join(ROOT, "packages/standard"), + fallbackBytes: 34456, + fallbackGzipBytes: 11735, + }); + + // 3. Competitor Benchmarks (fallbacks from npm/bundlephobia / isolated measurements) + const t3Engine = { + bytes: 14541, + gzipBytes: 4300, + }; + // In practice, developers using @t3-oss/env-core are locked into the monolithic Zod ecosystem (325.0 kB). + // While theoretical Standard Schema adapters could exist: + // - Shelved theoretical ArkType + T3: 164.0 kB (14.2 kB engine + 149.8 kB ArkType) + // - Shelved theoretical Valibot + T3: 27.6 kB (14.2 kB engine + 13.4 kB Valibot) + // We shelve those theoretical combinations: real-world T3 Env installations pay the full Zod tax. + // Pinning T3 Env to Zod across all tabs with "(requires Zod)" accurately reflects the baseline users migrate from. + const t3Zod = { + bytes: 332800, + gzipBytes: 67584, + }; + const varlock = { + bytes: 29082, + gzipBytes: 9318, + }; + + // 4. Construct rows for each validator tab + const results: BenchmarkData = { + arktype: [ + { + id: "arkenv-core", + name: "@arkenv/core", + npmPackage: "@arkenv/core", + engineBytes: coreEngine.bytes, + engineKb: toKb(coreEngine.bytes), + engineGzipBytes: coreEngine.gzipBytes, + engineGzipKb: toKb(coreEngine.gzipBytes), + validatorName: "ArkType", + validatorBytes: Math.max(0, coreArkType.bytes - coreEngine.bytes), + validatorKb: toKb(Math.max(0, coreArkType.bytes - coreEngine.bytes)), + validatorGzipBytes: Math.max( + 0, + coreArkType.gzipBytes - coreEngine.gzipBytes, + ), + validatorGzipKb: toKb( + Math.max(0, coreArkType.gzipBytes - coreEngine.gzipBytes), + ), + totalBytes: coreArkType.bytes, + totalKb: toKb(coreArkType.bytes), + totalGzipBytes: coreArkType.gzipBytes, + totalGzipKb: toKb(coreArkType.gzipBytes), + tier: "primary", + }, + { + id: "t3-env", + name: "@t3-oss/env-core", + npmPackage: "@t3-oss/env-core", + engineBytes: t3Engine.bytes, + engineKb: toKb(t3Engine.bytes), + engineGzipBytes: t3Engine.gzipBytes, + engineGzipKb: toKb(t3Engine.gzipBytes), + validatorName: "Zod", + validatorBytes: t3Zod.bytes - t3Engine.bytes, + validatorKb: toKb(t3Zod.bytes - t3Engine.bytes), + validatorGzipBytes: t3Zod.gzipBytes - t3Engine.gzipBytes, + validatorGzipKb: toKb(t3Zod.gzipBytes - t3Engine.gzipBytes), + totalBytes: t3Zod.bytes, + totalKb: toKb(t3Zod.bytes), + totalGzipBytes: t3Zod.gzipBytes, + totalGzipKb: toKb(t3Zod.gzipBytes), + tier: "competitor", + note: "requires Zod", + }, + { + id: "varlock", + name: "varlock", + npmPackage: "varlock", + engineBytes: varlock.bytes, + engineKb: toKb(varlock.bytes), + engineGzipBytes: varlock.gzipBytes, + engineGzipKb: toKb(varlock.gzipBytes), + totalBytes: varlock.bytes, + totalKb: toKb(varlock.bytes), + totalGzipBytes: varlock.gzipBytes, + totalGzipKb: toKb(varlock.gzipBytes), + tier: "reference", + note: "for reference", + }, + ], + zod: [ + { + id: "arkenv-standard", + name: "@arkenv/standard", + npmPackage: "@arkenv/standard", + engineBytes: standardEngine.bytes, + engineKb: toKb(standardEngine.bytes), + engineGzipBytes: standardEngine.gzipBytes, + engineGzipKb: toKb(standardEngine.gzipBytes), + validatorName: "Zod", + validatorBytes: Math.max(0, standardZod.bytes - standardEngine.bytes), + validatorKb: toKb( + Math.max(0, standardZod.bytes - standardEngine.bytes), + ), + validatorGzipBytes: Math.max( + 0, + standardZod.gzipBytes - standardEngine.gzipBytes, + ), + validatorGzipKb: toKb( + Math.max(0, standardZod.gzipBytes - standardEngine.gzipBytes), + ), + totalBytes: standardZod.bytes, + totalKb: toKb(standardZod.bytes), + totalGzipBytes: standardZod.gzipBytes, + totalGzipKb: toKb(standardZod.gzipBytes), + tier: "primary", + }, + { + id: "t3-env", + name: "@t3-oss/env-core", + npmPackage: "@t3-oss/env-core", + engineBytes: t3Engine.bytes, + engineKb: toKb(t3Engine.bytes), + engineGzipBytes: t3Engine.gzipBytes, + engineGzipKb: toKb(t3Engine.gzipBytes), + validatorName: "Zod", + validatorBytes: t3Zod.bytes - t3Engine.bytes, + validatorKb: toKb(t3Zod.bytes - t3Engine.bytes), + validatorGzipBytes: t3Zod.gzipBytes - t3Engine.gzipBytes, + validatorGzipKb: toKb(t3Zod.gzipBytes - t3Engine.gzipBytes), + totalBytes: t3Zod.bytes, + totalKb: toKb(t3Zod.bytes), + totalGzipBytes: t3Zod.gzipBytes, + totalGzipKb: toKb(t3Zod.gzipBytes), + tier: "competitor", + }, + { + id: "varlock", + name: "varlock", + npmPackage: "varlock", + engineBytes: varlock.bytes, + engineKb: toKb(varlock.bytes), + engineGzipBytes: varlock.gzipBytes, + engineGzipKb: toKb(varlock.gzipBytes), + totalBytes: varlock.bytes, + totalKb: toKb(varlock.bytes), + totalGzipBytes: varlock.gzipBytes, + totalGzipKb: toKb(varlock.gzipBytes), + tier: "reference", + note: "for reference", + }, + ], + valibot: [ + { + id: "arkenv-standard", + name: "@arkenv/standard", + npmPackage: "@arkenv/standard", + engineBytes: standardEngine.bytes, + engineKb: toKb(standardEngine.bytes), + engineGzipBytes: standardEngine.gzipBytes, + engineGzipKb: toKb(standardEngine.gzipBytes), + validatorName: "Valibot", + validatorBytes: Math.max( + 0, + standardValibot.bytes - standardEngine.bytes, + ), + validatorKb: toKb( + Math.max(0, standardValibot.bytes - standardEngine.bytes), + ), + validatorGzipBytes: Math.max( + 0, + standardValibot.gzipBytes - standardEngine.gzipBytes, + ), + validatorGzipKb: toKb( + Math.max(0, standardValibot.gzipBytes - standardEngine.gzipBytes), + ), + totalBytes: standardValibot.bytes, + totalKb: toKb(standardValibot.bytes), + totalGzipBytes: standardValibot.gzipBytes, + totalGzipKb: toKb(standardValibot.gzipBytes), + tier: "primary", + }, + { + id: "t3-env", + name: "@t3-oss/env-core", + npmPackage: "@t3-oss/env-core", + engineBytes: t3Engine.bytes, + engineKb: toKb(t3Engine.bytes), + engineGzipBytes: t3Engine.gzipBytes, + engineGzipKb: toKb(t3Engine.gzipBytes), + validatorName: "Zod", + validatorBytes: t3Zod.bytes - t3Engine.bytes, + validatorKb: toKb(t3Zod.bytes - t3Engine.bytes), + validatorGzipBytes: t3Zod.gzipBytes - t3Engine.gzipBytes, + validatorGzipKb: toKb(t3Zod.gzipBytes - t3Engine.gzipBytes), + totalBytes: t3Zod.bytes, + totalKb: toKb(t3Zod.bytes), + totalGzipBytes: t3Zod.gzipBytes, + totalGzipKb: toKb(t3Zod.gzipBytes), + tier: "competitor", + note: "requires Zod", + }, + { + id: "varlock", + name: "varlock", + npmPackage: "varlock", + engineBytes: varlock.bytes, + engineKb: toKb(varlock.bytes), + engineGzipBytes: varlock.gzipBytes, + engineGzipKb: toKb(varlock.gzipBytes), + totalBytes: varlock.bytes, + totalKb: toKb(varlock.bytes), + totalGzipBytes: varlock.gzipBytes, + totalGzipKb: toKb(varlock.gzipBytes), + tier: "reference", + note: "for reference", + }, + ], + matrix: { + valibot: { + engine: "@arkenv/standard/valibot", + subpath: "@arkenv/standard/valibot", + totalBytes: standardValibot.bytes, + totalKb: toKb(standardValibot.bytes), + gzipBytes: standardValibot.gzipBytes, + gzipKb: toKb(standardValibot.gzipBytes), + description: "Smallest edge footprint; modular functional tree-shaking", + }, + zodMini: { + engine: "@arkenv/standard/zod-mini", + subpath: "@arkenv/standard/zod-mini", + totalBytes: standardZodMini.bytes, + totalKb: toKb(standardZodMini.bytes), + gzipBytes: standardZodMini.gzipBytes, + gzipKb: toKb(standardZodMini.gzipBytes), + description: "~90% smaller than classic Zod; familiar syntax", + }, + arktype: { + engine: "@arkenv/core", + subpath: "@arkenv/core", + totalBytes: coreArkType.bytes, + totalKb: toKb(coreArkType.bytes), + gzipBytes: coreArkType.gzipBytes, + gzipKb: toKb(coreArkType.gzipBytes), + description: + "TypeScript-native DSL strings; built-in keywords; zero dependencies", + }, + classicZod: { + engine: "@arkenv/standard", + subpath: "@arkenv/standard", + totalBytes: standardZod.bytes, + totalKb: toKb(standardZod.bytes), + gzipBytes: standardZod.gzipBytes, + gzipKb: toKb(standardZod.gzipBytes), + description: "Drop-in compatibility for existing Zod schemas (Zod 4)", + }, + }, + }; + + if (!existsSync(dirname(OUT_PATH))) { + mkdirSync(dirname(OUT_PATH), { recursive: true }); + } + writeFileSync(OUT_PATH, `${JSON.stringify(results, null, "\t")}\n`); + console.log("Benchmark artifact successfully generated at:", OUT_PATH); +} + +run().catch((err) => { + console.error("Benchmark generation error:", err); + process.exit(1); +});