Skip to content

Commit 871000f

Browse files
authored
Move from zod to valibot to reduce the frontend bundle size (#4026)
This saves us about 30Kb gzipped, which is about 10% of the initial bundle size
2 parents 8fbf555 + be31201 commit 871000f

10 files changed

+88
-95
lines changed

frontend/package-lock.json

Lines changed: 17 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"@radix-ui/react-dialog": "^1.1.6",
2525
"@tanstack/react-query": "^5.66.0",
2626
"@tanstack/react-router": "^1.102.5",
27-
"@tanstack/router-zod-adapter": "^1.81.5",
2827
"@vector-im/compound-design-tokens": "3.0.1",
2928
"@vector-im/compound-web": "^7.6.2",
3029
"@zxcvbn-ts/core": "^3.0.4",
@@ -36,8 +35,8 @@
3635
"react-dom": "^19.0.0",
3736
"react-i18next": "^15.4.0",
3837
"swagger-ui-dist": "^5.18.3",
39-
"vaul": "^1.1.2",
40-
"zod": "^3.24.2"
38+
"valibot": "^1.0.0-rc.0",
39+
"vaul": "^1.1.2"
4140
},
4241
"devDependencies": {
4342
"@biomejs/biome": "^1.9.4",

frontend/src/pagination.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Please see LICENSE in the repository root for full details.
66

77
import { useState } from "react";
8-
import * as z from "zod";
8+
import * as v from "valibot";
99

1010
// PageInfo we get on connections from the GraphQL API
1111
type PageInfo = {
@@ -18,32 +18,32 @@ type PageInfo = {
1818
export const FIRST_PAGE = Symbol("FIRST_PAGE");
1919
const LAST_PAGE = Symbol("LAST_PAGE");
2020

21-
export const anyPaginationSchema = z.object({
22-
first: z.number().nullish(),
23-
after: z.string().nullish(),
24-
last: z.number().nullish(),
25-
before: z.string().nullish(),
21+
export const anyPaginationSchema = v.object({
22+
first: v.nullish(v.number()),
23+
after: v.nullish(v.string()),
24+
last: v.nullish(v.number()),
25+
before: v.nullish(v.string()),
2626
});
2727

28-
const forwardPaginationSchema = z.object({
29-
first: z.number(),
30-
after: z.string().nullish(),
28+
const forwardPaginationSchema = v.object({
29+
first: v.number(),
30+
after: v.nullish(v.string()),
3131
});
3232

33-
const backwardPaginationSchema = z.object({
34-
last: z.number(),
35-
before: z.string().nullish(),
33+
const backwardPaginationSchema = v.object({
34+
last: v.number(),
35+
before: v.nullish(v.string()),
3636
});
3737

38-
const paginationSchema = z.union([
38+
const paginationSchema = v.union([
3939
forwardPaginationSchema,
4040
backwardPaginationSchema,
4141
]);
4242

43-
type ForwardPagination = z.infer<typeof forwardPaginationSchema>;
44-
type BackwardPagination = z.infer<typeof backwardPaginationSchema>;
45-
export type Pagination = z.infer<typeof paginationSchema>;
46-
export type AnyPagination = z.infer<typeof anyPaginationSchema>;
43+
type ForwardPagination = v.InferOutput<typeof forwardPaginationSchema>;
44+
type BackwardPagination = v.InferOutput<typeof backwardPaginationSchema>;
45+
export type Pagination = v.InferOutput<typeof paginationSchema>;
46+
export type AnyPagination = v.InferOutput<typeof anyPaginationSchema>;
4747

4848
// Check if the pagination is a valid pagination
4949
const isValidPagination = (

frontend/src/routes/_account.index.tsx

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
import { queryOptions } from "@tanstack/react-query";
88
import { createFileRoute, redirect } from "@tanstack/react-router";
9-
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
10-
import * as z from "zod";
9+
import * as v from "valibot";
1110
import { query as userEmailListQuery } from "../components/UserProfile/UserEmailList";
1211
import { graphql } from "../gql";
1312
import { graphqlRequest } from "../graphql";
@@ -38,39 +37,39 @@ export const query = queryOptions({
3837
queryFn: ({ signal }) => graphqlRequest({ query: QUERY, signal }),
3938
});
4039

41-
const actionSchema = z
42-
.discriminatedUnion("action", [
43-
z.object({
44-
action: z.enum(["profile", "org.matrix.profile"]),
40+
const actionSchema = v.variant("action", [
41+
v.object({
42+
action: v.picklist(["profile", "org.matrix.profile"]),
43+
}),
44+
v.object({
45+
action: v.picklist(["sessions_list", "org.matrix.sessions_list"]),
46+
}),
47+
v.object({
48+
action: v.picklist(["session_view", "org.matrix.session_view"]),
49+
device_id: v.optional(v.string()),
50+
}),
51+
v.object({
52+
action: v.picklist(["session_end", "org.matrix.session_end"]),
53+
device_id: v.optional(v.string()),
54+
}),
55+
v.object({
56+
action: v.literal("org.matrix.cross_signing_reset"),
57+
}),
58+
v.partial(
59+
v.looseObject({
60+
action: v.never(),
4561
}),
46-
z.object({
47-
action: z.enum(["sessions_list", "org.matrix.sessions_list"]),
48-
}),
49-
z.object({
50-
action: z.enum(["session_view", "org.matrix.session_view"]),
51-
device_id: z.string().optional(),
52-
}),
53-
z.object({
54-
action: z.enum(["session_end", "org.matrix.session_end"]),
55-
device_id: z.string().optional(),
56-
}),
57-
z.object({
58-
action: z.literal("org.matrix.cross_signing_reset"),
59-
}),
60-
z.object({
61-
action: z.undefined(),
62-
}),
63-
])
64-
.catch({ action: undefined });
62+
),
63+
]);
6564

6665
export const Route = createFileRoute("/_account/")({
67-
validateSearch: zodSearchValidator(actionSchema),
66+
validateSearch: actionSchema,
6867

6968
beforeLoad({ search }) {
7069
switch (search.action) {
7170
case "profile":
7271
case "org.matrix.profile":
73-
throw redirect({ to: "/" });
72+
throw redirect({ to: "/", search: {} });
7473

7574
case "sessions_list":
7675
case "org.matrix.sessions_list":

frontend/src/routes/_account.sessions.browsers.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
// Please see LICENSE in the repository root for full details.
66

77
import { createFileRoute } from "@tanstack/react-router";
8-
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
9-
import * as z from "zod";
8+
import * as v from "valibot";
109

1110
import { queryOptions } from "@tanstack/react-query";
1211
import { graphql } from "../gql";
@@ -81,14 +80,15 @@ export const query = (pagination: AnyPagination, inactive: true | undefined) =>
8180
}),
8281
});
8382

84-
const searchSchema = z
85-
.object({
86-
inactive: z.literal(true).optional(),
87-
})
88-
.and(anyPaginationSchema);
83+
const searchSchema = v.intersect([
84+
v.object({
85+
inactive: v.optional(v.literal(true)),
86+
}),
87+
anyPaginationSchema,
88+
]);
8989

9090
export const Route = createFileRoute("/_account/sessions/browsers")({
91-
validateSearch: zodSearchValidator(searchSchema),
91+
validateSearch: searchSchema,
9292

9393
loaderDeps: ({ search: { inactive, ...pagination } }) => ({
9494
inactive,

frontend/src/routes/_account.sessions.index.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
// Please see LICENSE in the repository root for full details.
66

77
import { createFileRoute } from "@tanstack/react-router";
8-
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
9-
import * as z from "zod";
8+
import * as v from "valibot";
109

1110
import { queryOptions } from "@tanstack/react-query";
1211
import { graphql } from "../gql";
@@ -98,14 +97,15 @@ export const listQuery = (
9897
}),
9998
});
10099

101-
const searchSchema = z
102-
.object({
103-
inactive: z.literal(true).optional(),
104-
})
105-
.and(anyPaginationSchema);
100+
const searchSchema = v.intersect([
101+
v.object({
102+
inactive: v.optional(v.literal(true)),
103+
}),
104+
anyPaginationSchema,
105+
]);
106106

107107
export const Route = createFileRoute("/_account/sessions/")({
108-
validateSearch: zodSearchValidator(searchSchema),
108+
validateSearch: searchSchema,
109109

110110
loaderDeps: ({ search: { inactive, ...pagination } }) => ({
111111
inactive,

frontend/src/routes/emails.$id.in-use.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function EmailInUse(): React.ReactElement {
4848
})}
4949
/>
5050

51-
<ButtonLink as="a" Icon={IconArrowLeft} kind="tertiary" to="/">
51+
<ButtonLink Icon={IconArrowLeft} kind="tertiary" to="/">
5252
{t("action.back")}
5353
</ButtonLink>
5454
</Layout>

frontend/src/routes/emails.$id.verify.lazy.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function EmailVerify(): React.ReactElement {
189189
{t("frontend.verify_email.resend_code")}
190190
</Button>
191191

192-
<ButtonLink as="a" Icon={IconArrowLeft} kind="tertiary" to="/">
192+
<ButtonLink Icon={IconArrowLeft} kind="tertiary" to="/">
193193
{t("action.back")}
194194
</ButtonLink>
195195
</Form.Root>

frontend/src/routes/password.recovery.index.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
import { queryOptions } from "@tanstack/react-query";
88
import { createFileRoute, notFound } from "@tanstack/react-router";
9-
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
10-
import * as z from "zod";
9+
import * as v from "valibot";
1110
import { graphql } from "../gql";
1211
import { graphqlRequest } from "../graphql";
1312

@@ -31,12 +30,12 @@ export const query = (ticket: string) =>
3130
graphqlRequest({ query: QUERY, signal, variables: { ticket } }),
3231
});
3332

34-
const schema = z.object({
35-
ticket: z.string(),
33+
const schema = v.object({
34+
ticket: v.string(),
3635
});
3736

3837
export const Route = createFileRoute("/password/recovery/")({
39-
validateSearch: zodSearchValidator(schema),
38+
validateSearch: schema,
4039

4140
loaderDeps: ({ search: { ticket } }) => ({ ticket }),
4241

frontend/src/routes/reset-cross-signing.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ import {
88
Outlet,
99
createFileRoute,
1010
} from "@tanstack/react-router";
11-
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
1211
import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error";
1312
import { Button, Text } from "@vector-im/compound-web";
14-
import * as z from "zod";
13+
import * as v from "valibot";
1514

1615
import { useTranslation } from "react-i18next";
1716
import BlockList from "../components/BlockList";
1817
import Layout from "../components/Layout";
1918
import PageHeading from "../components/PageHeading";
2019

21-
const searchSchema = z.object({
22-
deepLink: z.boolean().optional(),
20+
const searchSchema = v.object({
21+
deepLink: v.optional(v.boolean()),
2322
});
2423

2524
export const Route = createFileRoute("/reset-cross-signing")({
25+
validateSearch: searchSchema,
2626
component: () => (
2727
<Layout>
2828
<BlockList>
@@ -31,7 +31,6 @@ export const Route = createFileRoute("/reset-cross-signing")({
3131
</Layout>
3232
),
3333
errorComponent: ResetCrossSigningError,
34-
validateSearch: zodSearchValidator(searchSchema),
3534
});
3635

3736
function ResetCrossSigningError({

0 commit comments

Comments
 (0)