Skip to content

Commit 54db793

Browse files
authored
Remove Typed Session Storage and dependency on Zod v3 (#530)
- Removes the deprecated Typed Session utils. - Removes the dependency on Zod for Sec-Fetch related utils, they now use plain JS to validate the header values. - This also removes the dependency on Zod as only Typed Cookie could use it but it depends on Standard Schema instead so any other lib can work
1 parent afca391 commit 54db793

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+250
-632
lines changed

README.md

Lines changed: 24 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ Additional optional dependencies may be needed, all optional dependencies are:
1919
- `is-ip`
2020
- `intl-parse-accept-language`
2121
- `react`
22-
- `zod`
2322

2423
The utils that require an extra optional dependency mention it in their documentation.
2524

2625
If you want to install them all run:
2726

2827
```sh
29-
npm add @edgefirst-dev/batcher @edgefirst-dev/jwt @edgefirst-dev/server-timing @oslojs/crypto @oslojs/encoding is-ip intl-parse-accept-language zod
28+
npm add @edgefirst-dev/batcher @edgefirst-dev/jwt @edgefirst-dev/server-timing @oslojs/crypto @oslojs/encoding is-ip intl-parse-accept-language
3029
```
3130

3231
React and React Router packages should be already installed in your project.
@@ -51,7 +50,7 @@ export async function loader({ request }: Route.LoaderArgs) {
5150
await promiseHash({
5251
user: getUser(request),
5352
posts: getPosts(request),
54-
})
53+
}),
5554
);
5655
}
5756
```
@@ -72,7 +71,7 @@ export async function loader({ request }: Route.LoaderArgs) {
7271
likes: getLikes(request),
7372
}),
7473
}),
75-
})
74+
}),
7675
);
7776
}
7877
```
@@ -104,7 +103,7 @@ try {
104103
let controller = new AbortController();
105104
let result = await timeout(
106105
fetch("https://example.com", { signal: controller.signal }),
107-
{ ms: 100, controller }
106+
{ ms: 100, controller },
108107
);
109108
} catch (error) {
110109
if (error instanceof TimeoutError) {
@@ -271,7 +270,7 @@ export default function handleRequest(
271270
request: Request,
272271
responseStatusCode: number,
273272
responseHeaders: Headers,
274-
remixContext: EntryContext
273+
remixContext: EntryContext,
275274
) {
276275
let callbackName = isbot(request.headers.get("user-agent"))
277276
? "onAllReady"
@@ -293,7 +292,7 @@ export default function handleRequest(
293292
new Response(body, {
294293
headers: responseHeaders,
295294
status: didError ? 500 : responseStatusCode,
296-
})
295+
}),
297296
).then((response) => {
298297
resolve(response);
299298
});
@@ -308,7 +307,7 @@ export default function handleRequest(
308307

309308
console.error(error);
310309
},
311-
}
310+
},
312311
);
313312

314313
setTimeout(abort, ABORT_DELAY);
@@ -317,7 +316,7 @@ export default function handleRequest(
317316

318317
export let handleDataRequest: HandleDataRequestFunction = async (
319318
response,
320-
{ request }
319+
{ request },
321320
) => {
322321
return await cors(request, response);
323322
};
@@ -461,7 +460,7 @@ export function useMarkAsRead() {
461460
return function submit(data) {
462461
fetcher.submit(
463462
{ csrf, ...data },
464-
{ action: "/api/mark-as-read", method: "post" }
463+
{ action: "/api/mark-as-read", method: "post" },
465464
);
466465
};
467466
}
@@ -1136,7 +1135,7 @@ export async function loader({ request }: Route.LoaderArgs) {
11361135
> [!NOTE]
11371136
> This depends on `@standard-schema/spec`, and React Router.
11381137
1139-
Cookie objects in Remix allows any type, the typed cookies from Remix Utils lets you use Zod to parse the cookie values and ensure they conform to a schema.
1138+
Cookie objects in Remix allows any type, the typed cookies from Remix Utils lets you use any Standard Schema compatible library to parse the cookie values and ensure they conform to a schema.
11401139

11411140
```ts
11421141
import { createCookie } from "react-router";
@@ -1166,7 +1165,7 @@ You could also use typed cookies with any sessionStorage mechanism from Remix.
11661165

11671166
```ts
11681167
let cookie = createCookie("session", cookieOptions);
1169-
let schema = z.object({ token: z.string() }).nullable();
1168+
let schema = z.object({ token: z.string().nullish() }).nullable();
11701169

11711170
let sessionStorage = createCookieSessionStorage({
11721171
cookie: createTypedCookie({ cookie, schema }),
@@ -1226,75 +1225,6 @@ await typedCookie.serialize("some fake url to pass schema validation", {
12261225
});
12271226
```
12281227

1229-
### Typed Sessions
1230-
1231-
> [!WARN]
1232-
> This util is marked as deprecated and will be removed in the next major version. Use the generic accepted by React Router's `createSessionStorage` helpers instead.
1233-
1234-
> [!NOTE]
1235-
> This depends on `zod`, and React Router.
1236-
1237-
Session objects in Remix allows any type, the typed sessions from Remix Utils lets you use Zod to parse the session data and ensure they conform to a schema.
1238-
1239-
```ts
1240-
import { createCookieSessionStorage } from "react-router";
1241-
import { createTypedSessionStorage } from "remix-utils/typed-session";
1242-
import { z } from "zod";
1243-
1244-
let schema = z.object({
1245-
token: z.string().optional(),
1246-
count: z.number().default(1),
1247-
});
1248-
1249-
// you can use a Remix's Cookie container or a Remix Utils' Typed Cookie container
1250-
let sessionStorage = createCookieSessionStorage({ cookie });
1251-
1252-
// pass the session storage and the schema
1253-
let typedSessionStorage = createTypedSessionStorage({ sessionStorage, schema });
1254-
```
1255-
1256-
Now you can use typedSessionStorage as a drop-in replacement for your normal sessionStorage.
1257-
1258-
```ts
1259-
let session = typedSessionStorage.getSession(request.headers.get("Cookie"));
1260-
1261-
session.get("token"); // this will be a string or undefined
1262-
session.get("count"); // this will be a number
1263-
session.get("random"); // this will make TS yell because it's not in the schema
1264-
1265-
session.has("token"); // this will be a boolean
1266-
session.has("count"); // this will be a boolean
1267-
1268-
// this will make TS yell because it's not a string, if you ignore it it will
1269-
// throw a ZodError
1270-
session.set("token", 123);
1271-
```
1272-
1273-
Now Zod will ensure the data you try to save to the session is valid by not allowing you to get, set or unset data.
1274-
1275-
> [!TIP]
1276-
> Remember that you either need to mark fields as optional or set a default value in the schema, otherwise it will be impossible to call getSession to get a new session object.
1277-
1278-
You can also use async refinements in your schemas because typed sesions uses parseAsync method from Zod.
1279-
1280-
```ts
1281-
let schema = z.object({
1282-
token: z
1283-
.string()
1284-
.optional()
1285-
.refine(async (token) => {
1286-
if (!token) return true; // handle optionallity
1287-
let user = await getUserByToken(token);
1288-
return user !== null;
1289-
}, "INVALID_TOKEN"),
1290-
});
1291-
1292-
let typedSessionStorage = createTypedSessionStorage({ sessionStorage, schema });
1293-
1294-
// this will throw if the token stored in the session is not valid anymore
1295-
typedSessionStorage.getSession(request.headers.get("Cookie"));
1296-
```
1297-
12981228
### Server-Sent Events
12991229

13001230
> [!NOTE]
@@ -1389,7 +1319,7 @@ export default function handleRequest(
13891319
request: Request,
13901320
responseStatusCode: number,
13911321
responseHeaders: Headers,
1392-
remixContext: EntryContext
1322+
remixContext: EntryContext,
13931323
) {
13941324
await rollingCookie(sessionCookie, request, responseHeaders);
13951325

@@ -1398,13 +1328,13 @@ export default function handleRequest(
13981328
request,
13991329
responseStatusCode,
14001330
responseHeaders,
1401-
remixContext
1331+
remixContext,
14021332
)
14031333
: handleBrowserRequest(
14041334
request,
14051335
responseStatusCode,
14061336
responseHeaders,
1407-
remixContext
1337+
remixContext,
14081338
);
14091339
}
14101340
```
@@ -1416,16 +1346,16 @@ import { rollingCookie } from "remix-utils/rolling-cookie";
14161346

14171347
export let handleDataRequest: HandleDataRequestFunction = async (
14181348
response: Response,
1419-
{ request }
1349+
{ request },
14201350
) => {
14211351
let cookieValue = await sessionCookie.parse(
1422-
responseHeaders.get("set-cookie")
1352+
responseHeaders.get("set-cookie"),
14231353
);
14241354
if (!cookieValue) {
14251355
cookieValue = await sessionCookie.parse(request.headers.get("cookie"));
14261356
responseHeaders.append(
14271357
"Set-Cookie",
1428-
await sessionCookie.serialize(cookieValue)
1358+
await sessionCookie.serialize(cookieValue),
14291359
);
14301360
}
14311361

@@ -1518,10 +1448,10 @@ export default function handleRequest(
15181448
request: Request,
15191449
statusCode: number,
15201450
headers: Headers,
1521-
context: EntryContext
1451+
context: EntryContext,
15221452
) {
15231453
let markup = renderToString(
1524-
<RemixServer context={context} url={request.url} />
1454+
<RemixServer context={context} url={request.url} />,
15251455
);
15261456
headers.set("Content-Type", "text/html");
15271457

@@ -1860,7 +1790,7 @@ Now, the `respondTo` function will check the `Accept` header and call the correc
18601790
import { parseAcceptHeader } from "remix-utils/parse-accept-header";
18611791

18621792
let parsed = parseAcceptHeader(
1863-
"text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, image/*, */*;q=0.8"
1793+
"text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, image/*, */*;q=0.8",
18641794
);
18651795
```
18661796

@@ -2116,7 +2046,7 @@ By default the middleware will automaticaly commit the session at the end of the
21162046
```ts
21172047
let [sessionMiddleware, getSession] = unstable_createSessionMiddleware(
21182048
sessionStorage,
2119-
shouldCommit
2049+
shouldCommit,
21202050
);
21212051
```
21222052

@@ -2129,7 +2059,7 @@ import { dequal } from "dequal";
21292059

21302060
let [sessionMiddleware, getSession] = unstable_createSessionMiddleware(
21312061
sessionStorage,
2132-
(previous, next) => !dequal(previous, next) // Only commit if session changed
2062+
(previous, next) => !dequal(previous, next), // Only commit if session changed
21332063
);
21342064
```
21352065

@@ -2140,7 +2070,7 @@ let [sessionMiddleware, getSession] = unstable_createSessionMiddleware(
21402070
sessionStorage,
21412071
(previous, next) => {
21422072
return current.user.id !== previous.user.id;
2143-
}
2073+
},
21442074
);
21452075
```
21462076

@@ -2921,7 +2851,7 @@ import { unstable_createRollingCookieMiddleware } from "remix-utils/middleware/r
29212851
import { cookie } from "~/cookies";
29222852

29232853
export const [rollingCookieMiddleware] = unstable_createRollingCookieMiddleware(
2924-
{ cookie }
2854+
{ cookie },
29252855
);
29262856
```
29272857

bun.lock

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"typedoc": "^0.28.12",
3434
"typedoc-plugin-mdn-links": "^5.0.9",
3535
"typescript": "^5.9.2",
36-
"zod": "^3.22.4",
36+
"zod": "^4.1.5",
3737
},
3838
"peerDependencies": {
3939
"@edgefirst-dev/batcher": "^1.0.0",
@@ -46,7 +46,6 @@
4646
"is-ip": "^5.0.1",
4747
"react": "^18.0.0 || ^19.0.0",
4848
"react-router": "^7.0.0",
49-
"zod": "^3.22.4",
5049
},
5150
"optionalPeers": [
5251
"@edgefirst-dev/batcher",
@@ -59,7 +58,6 @@
5958
"is-ip",
6059
"react",
6160
"react-router",
62-
"zod",
6361
],
6462
},
6563
},
@@ -448,7 +446,7 @@
448446

449447
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
450448

451-
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
449+
"zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="],
452450

453451
"@arethetypeswrong/core/typescript": ["typescript@5.6.1-rc", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ=="],
454452

package.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,6 @@
123123
"types": "./build/server/typed-cookie.d.ts",
124124
"default": "./build/server/typed-cookie.js"
125125
},
126-
"./typed-session": {
127-
"types": "./build/server/typed-session.d.ts",
128-
"default": "./build/server/typed-session.js"
129-
},
130126
"./client-only": {
131127
"types": "./build/react/client-only.d.ts",
132128
"default": "./build/react/client-only.js"
@@ -262,8 +258,7 @@
262258
"intl-parse-accept-language": "^1.0.0",
263259
"is-ip": "^5.0.1",
264260
"react": "^18.0.0 || ^19.0.0",
265-
"react-router": "^7.0.0",
266-
"zod": "^3.22.4"
261+
"react-router": "^7.0.0"
267262
},
268263
"peerDependenciesMeta": {
269264
"@edgefirst-dev/batcher": {
@@ -295,9 +290,6 @@
295290
},
296291
"react": {
297292
"optional": true
298-
},
299-
"zod": {
300-
"optional": true
301293
}
302294
},
303295
"devDependencies": {
@@ -327,7 +319,7 @@
327319
"typedoc": "^0.28.12",
328320
"typedoc-plugin-mdn-links": "^5.0.9",
329321
"typescript": "^5.9.2",
330-
"zod": "^3.22.4"
322+
"zod": "^4.1.5"
331323
},
332324
"dependencies": {
333325
"type-fest": "^4.41.0"

src/common/promise.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test";
2-
import { promiseHash, timeout } from "./promise";
2+
import { promiseHash, timeout } from "./promise.js";
33

44
describe(promiseHash, () => {
55
test("should await all promises in a hash and return them with the same name", async () => {

src/common/timers.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, setSystemTime, test } from "bun:test";
2-
import { interval } from "./timers";
2+
import { interval } from "./timers.js";
33

44
describe("Timers", () => {
55
describe(interval, () => {

src/react/fetcher-type.test.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { describe, expect, test } from "bun:test";
22
import { render, screen } from "@testing-library/react";
33
import { createRoutesStub, useFetcher } from "react-router";
4-
5-
import { getFetcherType, useFetcherType } from "./fetcher-type";
4+
import { getFetcherType, useFetcherType } from "./fetcher-type.js";
65

76
// biome-ignore lint/suspicious/noSkippedTests: Test pass with happy-dom but using it globally breaks other tests, so we skip it for now
87
describe.skip(getFetcherType.name, () => {

src/react/react.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { render } from "@testing-library/react";
33
import {
44
AuthenticityTokenInput,
55
AuthenticityTokenProvider,
6-
} from "./authenticity-token";
6+
} from "./authenticity-token.js";
77

88
// biome-ignore lint/suspicious/noSkippedTests: Fix this
99
describe.skip("React Utils", () => {

0 commit comments

Comments
 (0)