From 121a6682b735d4fd3940a052169fc384a18b5c98 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Wed, 1 Jul 2026 19:11:57 +0300 Subject: [PATCH 01/12] docs: add design spec for isomorphic getCookie/setCookie Spec for exposing createIsomorphicFn-based getCookie/setCookie from the root export of react-start, solid-start, and vue-start. --- .../2026-07-01-isomorphic-cookies-design.md | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md diff --git a/docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md b/docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md new file mode 100644 index 0000000000..cfff831942 --- /dev/null +++ b/docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md @@ -0,0 +1,111 @@ +# Isomorphic `getCookie` / `setCookie` — Design + +## Problem + +`getCookie` / `setCookie` currently only exist server-side, exported from +`@tanstack/{react,solid,vue}-start/server` (defined in +`packages/start-server-core/src/request-response.ts`). They throw if called +outside an active server request (no `H3Event` in `AsyncLocalStorage`), so +they cannot be called from client code or from isomorphic code paths that run +on both sides. + +## Goal + +Add isomorphic versions of `getCookie(name)` and `setCookie(name, value, options?)` +exported from the **root** of `@tanstack/react-start`, `@tanstack/solid-start`, +and `@tanstack/vue-start` (not `/server`), built with `createIsomorphicFn`, so +the same call works on the server (reads/writes the current request/response) +and on the client (reads/writes `document.cookie`). + +Same function names as the existing server-only versions — no collision, +since the import path differs (`@tanstack/react-start` vs +`@tanstack/react-start/server`). + +Out of scope: `getCookies` (plural, all cookies) and `deleteCookie` stay +server-only for now. `CookieSerializeOptions` is not re-exported as a type, +matching current `/server` behavior. + +## Why not `start-client-core` + +The root export of all three framework packages currently comes from +`export * from '@tanstack/start-client-core'`. That would be the natural home +for a shared isomorphic util, but `start-server-core`'s `package.json` already +declares a dependency on `start-client-core`. Adding the reverse dependency +(`start-client-core` → `start-server-core`, needed to call the existing server +`getCookie`/`setCookie`) would create a circular workspace dependency. + +`packages/react-start`, `packages/solid-start`, and `packages/vue-start` each +already depend on **both** `start-client-core` and `start-server-core` directly, +with no cycle. So the implementation is added once per framework package +(small, ~15-line file, duplicated 3x) instead of shared. This mirrors the +existing precedent where `useServerFn` is already an independent per-framework +file rather than a shared one. + +## Implementation + +New file `src/cookies.ts`, identical content in `packages/react-start`, +`packages/solid-start`, `packages/vue-start`: + +```ts +import { createIsomorphicFn } from '@tanstack/start-client-core' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +type GetCookieFn = (name: string) => string | undefined +type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +export const getCookie: GetCookieFn = createIsomorphicFn() + .server(getServerCookie) + .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + +export const setCookie: SetCookieFn = createIsomorphicFn() + .server(setServerCookie) + .client((name: string, value: string, options?: CookieSerializeOptions) => { + document.cookie = serialize(name, value, options) + }) as SetCookieFn +``` + +Exported from each package's root `index.ts` (alongside the existing +`useServerFn` export), e.g. in `packages/react-start/src/index.ts`: + +```ts +export { getCookie, setCookie } from './cookies' +``` + +### Behavior + +- **Server branch** delegates directly to the existing + `start-server-core` implementation — no server-side logic is duplicated. + Same request-scoped behavior as today (throws outside an active request). +- **Client branch** uses `cookie-es`'s `parse`/`serialize` against + `document.cookie`, giving the same option semantics (`path`, `maxAge`, + `expires`, `domain`, `secure`, `sameSite`, etc.) as the server. `httpOnly` is + silently ignored by the browser when set from client-side JS — expected, + unavoidable, and consistent with any client-side cookie write. +- The Start compiler strips whichever branch doesn't match the build target + (documented behavior of `createIsomorphicFn`), so `document.cookie` never + reaches the server bundle and the `start-server-core` import never reaches + the client bundle. + +### New dependency + +Add `cookie-es` (`^3.0.0`, matching `start-server-core`'s existing range) to +the `dependencies` of `react-start`, `solid-start`, and `vue-start` +`package.json`. + +## Testing + +There's no existing unit-test precedent for these small per-framework wrapper +files (`useServerFn`, `createCsrfMiddleware` have none either); verification +relies on typecheck/build passing. Existing e2e cookie fixtures at +`e2e/{react,solid,vue}-start/server-functions/src/routes/cookies/set.tsx` +are a candidate for extension to exercise the new root-level +`getCookie`/`setCookie`, but that's optional and not required for this change. \ No newline at end of file From 120f8e561f2ca641344bcd340fb5b5dacaac5239 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Wed, 1 Jul 2026 19:16:28 +0300 Subject: [PATCH 02/12] docs: add implementation plan for isomorphic getCookie/setCookie Task-by-task plan for docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md. --- .../plans/2026-07-01-isomorphic-cookies.md | 443 ++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-01-isomorphic-cookies.md diff --git a/docs/superpowers/plans/2026-07-01-isomorphic-cookies.md b/docs/superpowers/plans/2026-07-01-isomorphic-cookies.md new file mode 100644 index 0000000000..a8c2317d17 --- /dev/null +++ b/docs/superpowers/plans/2026-07-01-isomorphic-cookies.md @@ -0,0 +1,443 @@ +# Isomorphic getCookie/setCookie Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Export isomorphic `getCookie(name)` / `setCookie(name, value, options?)` from the root of `@tanstack/react-start`, `@tanstack/solid-start`, and `@tanstack/vue-start` (not `/server`), so the same call works on both client and server. + +**Architecture:** A new `src/cookies.ts` file, identical in content, added to each of the three framework packages. Each uses `createIsomorphicFn` from `@tanstack/start-client-core`: the `.server()` branch delegates to the existing `getCookie`/`setCookie` in `@tanstack/start-server-core`; the `.client()` branch uses `cookie-es`'s `parse`/`serialize` against `document.cookie`. Implemented per-framework (not in the shared `start-client-core`) because `start-server-core` already depends on `start-client-core`, and the reverse would be a circular workspace dependency. + +**Tech Stack:** TypeScript, `createIsomorphicFn` (`@tanstack/start-fn-stubs` via `@tanstack/start-client-core`), `cookie-es`, Vite (package build), publint/attw (package.json validation). + +**Design doc:** `docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md` + +--- + +### Task 1: Add `cookie-es` dependency to the three framework packages + +**Files:** +- Modify: `packages/react-start/package.json:170` +- Modify: `packages/solid-start/package.json:126` +- Modify: `packages/vue-start/package.json:117` + +- [ ] **Step 1: Add `cookie-es` to `packages/react-start/package.json`** + +Current (lines 162-172): +```json + "dependencies": { + "@tanstack/react-router": "workspace:*", + "@tanstack/react-start-client": "workspace:*", + "@tanstack/react-start-rsc": "workspace:*", + "@tanstack/react-start-server": "workspace:*", + "@tanstack/router-utils": "workspace:*", + "@tanstack/start-client-core": "workspace:*", + "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*", + "pathe": "^2.0.3" + }, +``` + +Change to: +```json + "dependencies": { + "@tanstack/react-router": "workspace:*", + "@tanstack/react-start-client": "workspace:*", + "@tanstack/react-start-rsc": "workspace:*", + "@tanstack/react-start-server": "workspace:*", + "@tanstack/router-utils": "workspace:*", + "@tanstack/start-client-core": "workspace:*", + "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*", + "cookie-es": "^3.0.0", + "pathe": "^2.0.3" + }, +``` + +- [ ] **Step 2: Add `cookie-es` to `packages/solid-start/package.json`** + +Current (lines 120-128): +```json + "dependencies": { + "@tanstack/solid-router": "workspace:*", + "@tanstack/solid-start-client": "workspace:*", + "@tanstack/solid-start-server": "workspace:*", + "@tanstack/start-client-core": "workspace:*", + "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*", + "pathe": "^2.0.3" + }, +``` + +Change to: +```json + "dependencies": { + "@tanstack/solid-router": "workspace:*", + "@tanstack/solid-start-client": "workspace:*", + "@tanstack/solid-start-server": "workspace:*", + "@tanstack/start-client-core": "workspace:*", + "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*", + "cookie-es": "^3.0.0", + "pathe": "^2.0.3" + }, +``` + +- [ ] **Step 3: Add `cookie-es` to `packages/vue-start/package.json`** + +Current (lines 114-122): +```json + "dependencies": { + "@tanstack/start-client-core": "workspace:*", + "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*", + "@tanstack/vue-router": "workspace:*", + "@tanstack/vue-start-client": "workspace:*", + "@tanstack/vue-start-server": "workspace:*", + "pathe": "^2.0.3" + }, +``` + +Change to: +```json + "dependencies": { + "@tanstack/start-client-core": "workspace:*", + "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*", + "@tanstack/vue-router": "workspace:*", + "@tanstack/vue-start-client": "workspace:*", + "@tanstack/vue-start-server": "workspace:*", + "cookie-es": "^3.0.0", + "pathe": "^2.0.3" + }, +``` + +- [ ] **Step 4: Install so the lockfile picks up the new dependency** + +Run: `pnpm install` +Expected: exits 0; `pnpm-lock.yaml` is modified to add `cookie-es` under `react-start`, `solid-start`, and `vue-start`. + +- [ ] **Step 5: Commit** + +```bash +git add packages/react-start/package.json packages/solid-start/package.json packages/vue-start/package.json pnpm-lock.yaml +git commit -m "chore: add cookie-es dependency to react-start, solid-start, vue-start" +``` + +--- + +### Task 2: Isomorphic cookies for `react-start` + +**Files:** +- Create: `packages/react-start/src/cookies.ts` +- Modify: `packages/react-start/src/index.ts:1` + +- [ ] **Step 1: Create `packages/react-start/src/cookies.ts`** + +```ts +import { createIsomorphicFn } from '@tanstack/start-client-core' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +type GetCookieFn = (name: string) => string | undefined +type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +/** + * Get a cookie value by name. Works on both the server (reads the current + * request's `Cookie` header) and the client (reads `document.cookie`). + * @param name Name of the cookie to get + * @returns Value of the cookie, or `undefined` if not present + * ```ts + * const authorization = getCookie('Authorization') + * ``` + */ +export const getCookie: GetCookieFn = createIsomorphicFn() + .server(getServerCookie) + .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + +/** + * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` + * header on the current response) and the client (writes `document.cookie`). + * @param name Name of the cookie to set + * @param value Value of the cookie to set + * @param options Options for serializing the cookie + * ```ts + * setCookie('Authorization', '1234567') + * ``` + */ +export const setCookie: SetCookieFn = createIsomorphicFn() + .server(setServerCookie) + .client((name: string, value: string, options?: CookieSerializeOptions) => { + document.cookie = serialize(name, value, options) + }) as SetCookieFn +``` + +- [ ] **Step 2: Export from `packages/react-start/src/index.ts`** + +Current (line 1): +```ts +export { useServerFn } from './useServerFn' +``` + +Change to: +```ts +export { useServerFn } from './useServerFn' +export { getCookie, setCookie } from './cookies' +``` + +- [ ] **Step 3: Build the package** + +Run: `pnpm --filter @tanstack/react-start run build` +Expected: exits 0, both `vite build` steps succeed with no TypeScript errors, `dist/esm/index.d.ts` includes `getCookie`/`setCookie`. + +- [ ] **Step 4: Verify package.json/type export validity** + +Run: `pnpm --filter @tanstack/react-start run test:build` +Expected: exits 0 (`publint --strict` and `attw` report no errors). + +- [ ] **Step 5: Commit** + +```bash +git add packages/react-start/src/cookies.ts packages/react-start/src/index.ts +git commit -m "feat(react-start): add isomorphic getCookie/setCookie to root export" +``` + +--- + +### Task 3: Isomorphic cookies for `solid-start` + +**Files:** +- Create: `packages/solid-start/src/cookies.ts` +- Modify: `packages/solid-start/src/index.ts:1` + +- [ ] **Step 1: Create `packages/solid-start/src/cookies.ts`** + +Identical to `packages/react-start/src/cookies.ts` from Task 2, Step 1 (same imports, same code — this package has the same dependencies available): + +```ts +import { createIsomorphicFn } from '@tanstack/start-client-core' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +type GetCookieFn = (name: string) => string | undefined +type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +/** + * Get a cookie value by name. Works on both the server (reads the current + * request's `Cookie` header) and the client (reads `document.cookie`). + * @param name Name of the cookie to get + * @returns Value of the cookie, or `undefined` if not present + * ```ts + * const authorization = getCookie('Authorization') + * ``` + */ +export const getCookie: GetCookieFn = createIsomorphicFn() + .server(getServerCookie) + .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + +/** + * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` + * header on the current response) and the client (writes `document.cookie`). + * @param name Name of the cookie to set + * @param value Value of the cookie to set + * @param options Options for serializing the cookie + * ```ts + * setCookie('Authorization', '1234567') + * ``` + */ +export const setCookie: SetCookieFn = createIsomorphicFn() + .server(setServerCookie) + .client((name: string, value: string, options?: CookieSerializeOptions) => { + document.cookie = serialize(name, value, options) + }) as SetCookieFn +``` + +- [ ] **Step 2: Export from `packages/solid-start/src/index.ts`** + +Current (line 1): +```ts +export { useServerFn } from './useServerFn' +``` + +Change to: +```ts +export { useServerFn } from './useServerFn' +export { getCookie, setCookie } from './cookies' +``` + +- [ ] **Step 3: Build the package** + +Run: `pnpm --filter @tanstack/solid-start run build` +Expected: exits 0, both `vite build` steps succeed with no TypeScript errors, `dist/esm/index.d.ts` includes `getCookie`/`setCookie`. + +- [ ] **Step 4: Verify package.json/type export validity** + +Run: `pnpm --filter @tanstack/solid-start run test:build` +Expected: exits 0 (`publint --strict` and `attw` report no errors). + +- [ ] **Step 5: Commit** + +```bash +git add packages/solid-start/src/cookies.ts packages/solid-start/src/index.ts +git commit -m "feat(solid-start): add isomorphic getCookie/setCookie to root export" +``` + +--- + +### Task 4: Isomorphic cookies for `vue-start` + +**Files:** +- Create: `packages/vue-start/src/cookies.ts` +- Modify: `packages/vue-start/src/index.ts:1` + +- [ ] **Step 1: Create `packages/vue-start/src/cookies.ts`** + +Identical to `packages/react-start/src/cookies.ts` from Task 2, Step 1: + +```ts +import { createIsomorphicFn } from '@tanstack/start-client-core' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +type GetCookieFn = (name: string) => string | undefined +type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +/** + * Get a cookie value by name. Works on both the server (reads the current + * request's `Cookie` header) and the client (reads `document.cookie`). + * @param name Name of the cookie to get + * @returns Value of the cookie, or `undefined` if not present + * ```ts + * const authorization = getCookie('Authorization') + * ``` + */ +export const getCookie: GetCookieFn = createIsomorphicFn() + .server(getServerCookie) + .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + +/** + * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` + * header on the current response) and the client (writes `document.cookie`). + * @param name Name of the cookie to set + * @param value Value of the cookie to set + * @param options Options for serializing the cookie + * ```ts + * setCookie('Authorization', '1234567') + * ``` + */ +export const setCookie: SetCookieFn = createIsomorphicFn() + .server(setServerCookie) + .client((name: string, value: string, options?: CookieSerializeOptions) => { + document.cookie = serialize(name, value, options) + }) as SetCookieFn +``` + +- [ ] **Step 2: Export from `packages/vue-start/src/index.ts`** + +Current (line 1): +```ts +export { useServerFn } from './useServerFn' +``` + +Change to: +```ts +export { useServerFn } from './useServerFn' +export { getCookie, setCookie } from './cookies' +``` + +- [ ] **Step 3: Build the package** + +Run: `pnpm --filter @tanstack/vue-start run build` +Expected: exits 0, both `vite build` steps succeed with no TypeScript errors, `dist/esm/index.d.ts` includes `getCookie`/`setCookie`. + +- [ ] **Step 4: Verify package.json/type export validity** + +Run: `pnpm --filter @tanstack/vue-start run test:build` +Expected: exits 0 (`publint --strict` and `attw` report no errors). + +- [ ] **Step 5: Commit** + +```bash +git add packages/vue-start/src/cookies.ts packages/vue-start/src/index.ts +git commit -m "feat(vue-start): add isomorphic getCookie/setCookie to root export" +``` + +--- + +### Task 5: Manual smoke check in the `server-functions` e2e app + +**Files:** +- Temporarily modify (revert after verifying): `e2e/react-start/server-functions/src/routes/cookies/set.tsx:1-3` + +The existing fixture at `e2e/react-start/server-functions/src/routes/cookies/set.tsx` already sets cookies on the server (via `setCookie` from `@tanstack/react-start/server`) and reads them back on the client (via the `js-cookie` package, in `RouteComponent`'s `useEffect`). This step temporarily swaps that fixture to use the new isomorphic export end-to-end, to confirm the server-set cookie and a client-set cookie both round-trip correctly through the real dev server. + +- [ ] **Step 1: Temporarily switch the fixture's import** + +In `e2e/react-start/server-functions/src/routes/cookies/set.tsx`, change: + +```ts +import { setCookie } from '@tanstack/react-start/server' +``` + +to: + +```ts +import { getCookie, setCookie } from '@tanstack/react-start' +``` + +- [ ] **Step 2: Add a client-side call to the same `setCookie`/`getCookie` in `RouteComponent`** + +Replace the `useEffect` body in `RouteComponent` (currently reads via `Cookies.get`) with a call that also exercises the isomorphic `setCookie`/`getCookie` from the client: + +```tsx +useEffect(() => { + setCookie(`cookie-5-${expectedCookieValue}`, expectedCookieValue) + const tempCookies: Record = {} + for (let i = 1; i <= 4; i++) { + const key = `cookie-${i}-${expectedCookieValue}` + tempCookies[key] = Cookies.get(key) + } + tempCookies[`cookie-5-${expectedCookieValue}`] = getCookie( + `cookie-5-${expectedCookieValue}`, + ) + setCookiesFromDocument(tempCookies) +}, []) +``` + +- [ ] **Step 3: Run the dev server and check the route in a browser** + +Run: `pnpm --filter tanstack-react-start-e2e-server-functions run dev` + +Visit `/cookies/set?value=test123` in a browser. Expected: the results table shows `cookie-1-test123` through `cookie-4-test123` with value `test123` (server-set, `/server` `setCookie` — unchanged path, confirms no regression), and `cookie-5-test123` also shows `test123` (set via the new isomorphic client `setCookie` and read back via the new isomorphic client `getCookie`, both from `@tanstack/react-start` root — confirms the client branch works). + +Stop the dev server (Ctrl+C) once confirmed. + +- [ ] **Step 4: Revert the temporary fixture changes** + +```bash +git checkout -- e2e/react-start/server-functions/src/routes/cookies/set.tsx +``` + +Expected: `git status` shows no changes in the e2e app (this task never commits anything — it's a manual verification-only step for this plan, not a permanent test addition). From d7fb8c21192e61f7dbbb9fc22c2b79eef15566f9 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Wed, 1 Jul 2026 19:17:32 +0300 Subject: [PATCH 03/12] chore: ignore .worktrees directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1699592784..53129d1368 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vite +.worktrees # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies From d139d1cbe2cd8024ca8fb33f1ab546245b8396e8 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Wed, 1 Jul 2026 19:27:04 +0300 Subject: [PATCH 04/12] chore: add cookie-es dependency to react-start, solid-start, vue-start --- packages/react-start/package.json | 1 + packages/solid-start/package.json | 1 + packages/vue-start/package.json | 1 + pnpm-lock.yaml | 289 ++++++++++++++---------------- 4 files changed, 142 insertions(+), 150 deletions(-) diff --git a/packages/react-start/package.json b/packages/react-start/package.json index b037e44f8f..0a61e9268f 100644 --- a/packages/react-start/package.json +++ b/packages/react-start/package.json @@ -168,6 +168,7 @@ "@tanstack/start-client-core": "workspace:*", "@tanstack/start-plugin-core": "workspace:*", "@tanstack/start-server-core": "workspace:*", + "cookie-es": "^3.0.0", "pathe": "^2.0.3" }, "peerDependencies": { diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index 4b11e9a219..8689a791a3 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -124,6 +124,7 @@ "@tanstack/start-client-core": "workspace:*", "@tanstack/start-plugin-core": "workspace:*", "@tanstack/start-server-core": "workspace:*", + "cookie-es": "^3.0.0", "pathe": "^2.0.3" }, "devDependencies": { diff --git a/packages/vue-start/package.json b/packages/vue-start/package.json index 5afd0d06a5..ff786c1fae 100644 --- a/packages/vue-start/package.json +++ b/packages/vue-start/package.json @@ -118,6 +118,7 @@ "@tanstack/vue-router": "workspace:*", "@tanstack/vue-start-client": "workspace:*", "@tanstack/vue-start-server": "workspace:*", + "cookie-es": "^3.0.0", "pathe": "^2.0.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a7c3a7348..0c35eb0b35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,7 +78,7 @@ importers: version: 1.26.2(eslint@9.22.0(jiti@2.7.0))(ts-api-utils@2.4.0(typescript@6.0.2))(typescript@6.0.2) '@nx/devkit': specifier: 22.7.5 - version: 22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))) + version: 22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3)) '@playwright/test': specifier: ^1.57.0 version: 1.58.0 @@ -138,7 +138,7 @@ importers: version: 4.0.3 nx: specifier: 22.7.5 - version: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)) + version: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3) prettier: specifier: ^3.8.0 version: 3.8.1 @@ -281,7 +281,7 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^5.5.0 - version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) '@platformatic/flame': specifier: ^1.6.0 version: 1.6.0 @@ -342,7 +342,7 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^5.5.0 - version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) '@datadog/pprof': specifier: ^5.13.2 version: 5.13.2 @@ -415,7 +415,7 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^5.5.0 - version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) '@datadog/pprof': specifier: ^5.13.2 version: 5.13.2 @@ -476,7 +476,7 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^5.5.0 - version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) @@ -9807,7 +9807,7 @@ importers: version: 0.5.20(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: npm:nitro-nightly@latest - version: nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: nitro-nightly@4.0.0-20251010-091516-7cafddba(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.1.18 version: 4.2.2 @@ -12708,6 +12708,9 @@ importers: '@tanstack/start-server-core': specifier: workspace:* version: link:../start-server-core + cookie-es: + specifier: ^3.0.0 + version: 3.1.1 pathe: specifier: ^2.0.3 version: 2.0.3 @@ -13206,6 +13209,9 @@ importers: '@tanstack/start-server-core': specifier: workspace:* version: link:../start-server-core + cookie-es: + specifier: ^3.0.0 + version: 3.1.1 pathe: specifier: ^2.0.3 version: 2.0.3 @@ -13601,6 +13607,9 @@ importers: '@tanstack/vue-start-server': specifier: workspace:* version: link:../vue-start-server + cookie-es: + specifier: ^3.0.0 + version: 3.1.1 pathe: specifier: ^2.0.3 version: 2.0.3 @@ -21793,21 +21802,6 @@ packages: miniflare: optional: true - env-runner@0.1.9: - resolution: {integrity: sha512-W9AiZlPx0uXtghAJiTBkeZOgyQdecVvoln3cHoOEZswPq0cVMi+WBhUQjdUn+JcZFAFgOt+i5fcO7C2zniZoCg==} - hasBin: true - peerDependencies: - '@netlify/runtime': ^4.1.23 - '@vercel/queue': ^0.2.0 - miniflare: ^4.20260515.0 - peerDependenciesMeta: - '@netlify/runtime': - optional: true - '@vercel/queue': - optional: true - miniflare: - optional: true - envinfo@7.14.0: resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} engines: {node: '>=4'} @@ -22586,18 +22580,17 @@ packages: crossws: optional: true - h3@2.0.1-rc.20: - resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} + h3@2.0.1-rc.2: + resolution: {integrity: sha512-2vS7OETzPDzGQxmmcs6ttu7p0NW25zAdkPXYOr43dn4GZf81uUljJvupa158mcpUGpsQUqIy4O4THWUQT1yVeA==} engines: {node: '>=20.11.1'} - hasBin: true peerDependencies: crossws: ^0.4.1 peerDependenciesMeta: crossws: optional: true - h3@2.0.1-rc.22: - resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} + h3@2.0.1-rc.20: + resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} engines: {node: '>=20.11.1'} hasBin: true peerDependencies: @@ -22676,9 +22669,6 @@ packages: hookable@6.1.0: resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} - hookable@6.1.1: - resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} - hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -22787,9 +22777,6 @@ packages: httpxy@0.3.1: resolution: {integrity: sha512-XjG/CEoofEisMrnFr0D6U6xOZ4mRfnwcYQ9qvvnT4lvnX8BoeA3x3WofB75D+vZwpaobFVkBIHrZzoK40w8XSw==} - httpxy@0.5.3: - resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==} - human-id@4.1.1: resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} hasBin: true @@ -23875,45 +23862,30 @@ packages: netlify-redirector@0.5.0: resolution: {integrity: sha512-4zdzIP+6muqPCuE8avnrgDJ6KW/2+UpHTRcTbMXCIRxiRmyrX+IZ4WSJGZdHPWF3WmQpXpy603XxecZ9iygN7w==} + nf3@0.1.12: + resolution: {integrity: sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ==} + nf3@0.3.11: resolution: {integrity: sha512-ObKp/SA3f1g1f/OMeDlRWaZmqGgk7A0NnDIbeO7c/MV4r/quMlpP/BsqMGuTi3lUlXbC1On8YH7ICM2u2bIAOw==} - nf3@0.3.17: - resolution: {integrity: sha512-N9zEWySuJFw+gR0lhS5863YsvNeudOdqRyFvNb+jMXbeTJOdrjDqkCpDginIZfUm0LzT1t1nCRiDeqQm/8kirQ==} - nf3@0.3.6: resolution: {integrity: sha512-/XRUUILTAyuy1XunyVQuqGp8aEmZ2TfRTn8Rji+FA4xqv20qzL4jV7Reqbuey2XucKgPeRVcEYGScmJM0UnB6Q==} - nitro-nightly@3.0.260522-beta: - resolution: {integrity: sha512-Pm0AiQ1nLcreUFZKJcmVI4l9K/ygXoFamIPhc44XJHj9vmt25Rlqjv55frw6sS0L1qHrySpo3xyVVBmR9aTBqA==} + nitro-nightly@4.0.0-20251010-091516-7cafddba: + resolution: {integrity: sha512-biADkmoR/Nb9OmUURTfDI2BpGo2IMEt2RbsiMhjqdwz2w3tEm+tx0Hd28LXjBCwid+l8jpqyfmpwc8jzwdng3w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@vercel/queue': ^0.2.0 - dotenv: '*' - giget: '*' - jiti: ^2.6.1 - rollup: ^4.60.3 + rolldown: '*' vite: ^8.0.14 xml2js: ^0.6.2 - zephyr-agent: ^0.2.0 peerDependenciesMeta: - '@vercel/queue': - optional: true - dotenv: - optional: true - giget: - optional: true - jiti: - optional: true - rollup: + rolldown: optional: true vite: optional: true xml2js: optional: true - zephyr-agent: - optional: true nitro@3.0.1-alpha.2: resolution: {integrity: sha512-YviDY5J/trS821qQ1fpJtpXWIdPYiOizC/meHavlm1Hfuhx//H+Egd1+4C5SegJRgtWMnRPW9n//6Woaw81cTQ==} @@ -24117,9 +24089,6 @@ packages: ocache@0.1.2: resolution: {integrity: sha512-lI34wjM7cahEdrq2I5obbF7MEdE97vULf6vNj6ZCzwEadzyXO1w7QOl2qzzG4IL8cyO7wDtXPj9CqW/aG3mn7g==} - ocache@0.1.4: - resolution: {integrity: sha512-e7geNdWjxSnvsSgvLuPvgKgu7ubM10ZmTPOgpr7mz2BXYtvjMKTiLhjFi/gWU8chkuP6hNkZBsa9LzOusyaqkQ==} - ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -24923,6 +24892,10 @@ packages: renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + rendu@0.0.6: + resolution: {integrity: sha512-nZ512Dw0MxKiIYfCVv8DPe6ig4m0Qt3FOYBJEXrammjIYBBPuHaudc0AGfYx+iyOw2q0itAtPywiVZXtTFCsig==} + hasBin: true + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -25432,6 +25405,11 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + srvx@0.8.16: + resolution: {integrity: sha512-hmcGW4CgroeSmzgF1Ihwgl+Ths0JqAJ7HwjP2X7e3JzY7u4IydLMcdnlqGQiQGUswz+PO9oh/KtCpOISIvs9QQ==} + engines: {node: '>=20.16.0'} + hasBin: true + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -25843,6 +25821,7 @@ packages: tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -26005,6 +25984,9 @@ packages: resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.21: + resolution: {integrity: sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -26120,32 +26102,32 @@ packages: uploadthing: optional: true - unstorage@2.0.0-alpha.5: - resolution: {integrity: sha512-Sj8btci21Twnd6M+N+MHhjg3fVn6lAPElPmvFTe0Y/wR0WImErUdA1PzlAaUavHylJ7uDiFwlZDQKm0elG4b7g==} + unstorage@2.0.0-alpha.3: + resolution: {integrity: sha512-BeoqISVh8jxqnPseHH7/92twe2VkQztrudXg8RFZVbXb4ckkFdpLk1LnNvsUndDltyodBMVxgI6V7JcbJYt2VQ==} peerDependencies: - '@azure/app-configuration': ^1.9.0 - '@azure/cosmos': ^4.7.0 - '@azure/data-tables': ^13.3.1 - '@azure/identity': ^4.13.0 - '@azure/keyvault-secrets': ^4.10.0 - '@azure/storage-blob': ^12.29.1 + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 '@capacitor/preferences': ^6.0.3 || ^7.0.0 - '@deno/kv': '>=0.12.0' + '@deno/kv': '>=0.9.0' '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.35.6 - '@vercel/blob': '>=0.27.3' + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' '@vercel/functions': ^2.2.12 || ^3.0.0 '@vercel/kv': ^1.0.1 aws4fetch: ^1.0.20 - chokidar: ^4 || ^5 - db0: '>=0.3.4' - idb-keyval: ^6.2.2 - ioredis: ^5.8.2 + chokidar: ^4.0.3 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 lru-cache: ^11.2.2 - mongodb: ^6 || ^7 - ofetch: '*' - uploadthing: ^7.7.4 + mongodb: ^6.20.0 + ofetch: ^1.4.1 + uploadthing: ^7.4.4 peerDependenciesMeta: '@azure/app-configuration': optional: true @@ -26194,20 +26176,20 @@ packages: uploadthing: optional: true - unstorage@2.0.0-alpha.6: - resolution: {integrity: sha512-w5vLYCJtnSx3OBtDk7cG4c1p3dfAnHA4WSZq9Xsurjbl2wMj7zqfOIjaHQI1Bl7yKzUxXAi+kbMr8iO2RhJmBA==} + unstorage@2.0.0-alpha.5: + resolution: {integrity: sha512-Sj8btci21Twnd6M+N+MHhjg3fVn6lAPElPmvFTe0Y/wR0WImErUdA1PzlAaUavHylJ7uDiFwlZDQKm0elG4b7g==} peerDependencies: - '@azure/app-configuration': ^1.11.0 - '@azure/cosmos': ^4.9.1 - '@azure/data-tables': ^13.3.2 + '@azure/app-configuration': ^1.9.0 + '@azure/cosmos': ^4.7.0 + '@azure/data-tables': ^13.3.1 '@azure/identity': ^4.13.0 '@azure/keyvault-secrets': ^4.10.0 - '@azure/storage-blob': ^12.31.0 - '@capacitor/preferences': ^6 || ^7 || ^8 - '@deno/kv': '>=0.13.0' + '@azure/storage-blob': ^12.29.1 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.12.0' '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.36.2 + '@upstash/redis': ^1.35.6 '@vercel/blob': '>=0.27.3' '@vercel/functions': ^2.2.12 || ^3.0.0 '@vercel/kv': ^1.0.1 @@ -26215,8 +26197,8 @@ packages: chokidar: ^4 || ^5 db0: '>=0.3.4' idb-keyval: ^6.2.2 - ioredis: ^5.9.3 - lru-cache: ^11.2.6 + ioredis: ^5.8.2 + lru-cache: ^11.2.2 mongodb: ^6 || ^7 ofetch: '*' uploadthing: ^7.7.4 @@ -26268,8 +26250,8 @@ packages: uploadthing: optional: true - unstorage@2.0.0-alpha.7: - resolution: {integrity: sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog==} + unstorage@2.0.0-alpha.6: + resolution: {integrity: sha512-w5vLYCJtnSx3OBtDk7cG4c1p3dfAnHA4WSZq9Xsurjbl2wMj7zqfOIjaHQI1Bl7yKzUxXAi+kbMr8iO2RhJmBA==} peerDependencies: '@azure/app-configuration': ^1.11.0 '@azure/cosmos': ^4.9.1 @@ -27700,7 +27682,7 @@ snapshots: '@bramus/specificity@2.4.2': dependencies: - css-tree: 3.1.0 + css-tree: 3.2.1 '@bundled-es-modules/cookie@2.0.1': dependencies: @@ -27708,7 +27690,7 @@ snapshots: '@bundled-es-modules/statuses@1.0.1': dependencies: - statuses: 2.0.1 + statuses: 2.0.2 '@bundled-es-modules/tough-cookie@0.1.6': dependencies: @@ -28019,9 +28001,9 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260317.1': optional: true - '@codspeed/core@5.5.0': + '@codspeed/core@5.5.0(debug@4.4.3)': dependencies: - axios: 1.17.0 + axios: 1.17.0(debug@4.4.3) find-up: 6.3.0 form-data: 4.0.5 node-gyp-build: 4.8.4 @@ -28030,9 +28012,9 @@ snapshots: - debug - supports-color - '@codspeed/vitest-plugin@5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)': + '@codspeed/vitest-plugin@5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)': dependencies: - '@codspeed/core': 5.5.0 + '@codspeed/core': 5.5.0(debug@4.4.3) tinybench: 2.9.0 vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) @@ -30236,13 +30218,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.0 - '@nx/devkit@22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)))': + '@nx/devkit@22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.5 - nx: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)) + nx: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3) semver: 7.8.2 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -33551,7 +33533,7 @@ snapshots: debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.4.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: @@ -34050,7 +34032,7 @@ snapshots: '@vue/compiler-core@3.5.25': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@vue/shared': 3.5.25 entities: 4.5.0 estree-walker: 2.0.2 @@ -34063,14 +34045,14 @@ snapshots: '@vue/compiler-sfc@3.5.25': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@vue/compiler-core': 3.5.25 '@vue/compiler-dom': 3.5.25 '@vue/compiler-ssr': 3.5.25 '@vue/shared': 3.5.25 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.8 + postcss: 8.5.15 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.25': @@ -34597,7 +34579,7 @@ snapshots: aws-ssl-profiles@1.1.2: {} - axios@1.16.0: + axios@1.16.0(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.5 @@ -34605,7 +34587,7 @@ snapshots: transitivePeerDependencies: - debug - axios@1.17.0: + axios@1.17.0(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.5 @@ -35366,6 +35348,11 @@ snapshots: crossws@0.4.5(srvx@0.11.15): optionalDependencies: srvx: 0.11.15 + optional: true + + crossws@0.4.5(srvx@0.8.16): + optionalDependencies: + srvx: 0.8.16 css-loader@7.1.2(webpack@5.97.1): dependencies: @@ -35826,13 +35813,6 @@ snapshots: optionalDependencies: miniflare: 4.20260317.0 - env-runner@0.1.9: - dependencies: - crossws: 0.4.5(srvx@0.11.15) - exsolve: 1.0.8 - httpxy: 0.5.3 - srvx: 0.11.15 - envinfo@7.14.0: {} environment@1.1.0: {} @@ -36946,14 +36926,16 @@ snapshots: optionalDependencies: crossws: 0.4.4(srvx@0.11.15) - h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)): + h3@2.0.1-rc.2(crossws@0.4.5(srvx@0.8.16)): dependencies: - rou3: 0.8.1 - srvx: 0.11.15 + cookie-es: 2.0.0 + fetchdts: 0.1.7 + rou3: 0.7.12 + srvx: 0.8.16 optionalDependencies: - crossws: 0.4.5(srvx@0.11.15) + crossws: 0.4.5(srvx@0.8.16) - h3@2.0.1-rc.22(crossws@0.4.5(srvx@0.11.15)): + h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)): dependencies: rou3: 0.8.1 srvx: 0.11.15 @@ -37014,8 +36996,6 @@ snapshots: hookable@6.1.0: {} - hookable@6.1.1: {} - hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -37166,8 +37146,6 @@ snapshots: httpxy@0.3.1: {} - httpxy@0.5.3: {} - human-id@4.1.1: {} human-signals@5.0.0: {} @@ -37281,7 +37259,7 @@ snapshots: pathe: 2.0.3 sharp: 0.34.4 svgo: 4.0.0 - ufo: 1.6.1 + ufo: 1.6.3 unstorage: 1.17.4(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2) xss: 1.0.15 transitivePeerDependencies: @@ -38241,32 +38219,33 @@ snapshots: netlify-redirector@0.5.0: {} - nf3@0.3.11: {} + nf3@0.1.12: {} - nf3@0.3.17: {} + nf3@0.3.11: {} nf3@0.3.6: {} - nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro-nightly@4.0.0-20251010-091516-7cafddba(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: consola: 3.4.2 - crossws: 0.4.5(srvx@0.11.15) + cookie-es: 2.0.0 + crossws: 0.4.5(srvx@0.8.16) db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) - env-runner: 0.1.9 - h3: 2.0.1-rc.22(crossws@0.4.5(srvx@0.11.15)) - hookable: 6.1.1 - nf3: 0.3.17 - ocache: 0.1.4 - ofetch: 2.0.0-alpha.3 + esbuild: 0.25.10 + fetchdts: 0.1.7 + h3: 2.0.1-rc.2(crossws@0.4.5(srvx@0.8.16)) + jiti: 2.7.0 + nf3: 0.1.12 + ofetch: 1.5.1 ohash: 2.0.11 - rolldown: 1.0.2 - srvx: 0.11.15 - unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) + rendu: 0.0.6 + rollup: 4.56.0 + srvx: 0.8.16 + undici: 7.27.2 + unenv: 2.0.0-rc.21 + unstorage: 2.0.0-alpha.3(@netlify/blobs@10.1.0)(chokidar@4.0.3)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@1.5.1) optionalDependencies: - dotenv: 17.4.2 - giget: 2.0.0 - jiti: 2.7.0 + rolldown: 1.0.2 vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - '@azure/app-configuration' @@ -38280,7 +38259,6 @@ snapshots: - '@electric-sql/pglite' - '@libsql/client' - '@netlify/blobs' - - '@netlify/runtime' - '@planetscale/database' - '@upstash/redis' - '@vercel/blob' @@ -38293,7 +38271,6 @@ snapshots: - idb-keyval - ioredis - lru-cache - - miniflare - mongodb - mysql2 - sqlite3 @@ -38585,7 +38562,7 @@ snapshots: nwsapi@2.2.16: {} - nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)): + nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3): dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -38600,7 +38577,7 @@ snapshots: ansi-styles: 4.3.0 argparse: 2.0.1 asynckit: 0.4.0 - axios: 1.16.0 + axios: 1.16.0(debug@4.4.3) balanced-match: 4.0.3 base64-js: 1.5.1 bl: 4.1.0 @@ -38751,10 +38728,6 @@ snapshots: dependencies: ohash: 2.0.11 - ocache@0.1.4: - dependencies: - ohash: 2.0.11 - ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -39697,6 +39670,11 @@ snapshots: lodash: 4.17.21 strip-ansi: 6.0.1 + rendu@0.0.6: + dependencies: + cookie-es: 2.0.0 + srvx: 0.8.16 + repeat-string@1.6.1: {} require-directory@2.1.1: {} @@ -40329,6 +40307,8 @@ snapshots: srvx@0.11.15: {} + srvx@0.8.16: {} + stable-hash-x@0.2.0: {} stack-trace@0.0.10: {} @@ -40850,6 +40830,14 @@ snapshots: undici@7.27.2: {} + unenv@2.0.0-rc.21: + dependencies: + defu: 6.1.4 + exsolve: 1.0.8 + ohash: 2.0.11 + pathe: 2.0.3 + ufo: 1.6.3 + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -40951,24 +40939,25 @@ snapshots: db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) ioredis: 5.9.2 - unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.3(@netlify/blobs@10.1.0)(chokidar@4.0.3)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@1.5.1): optionalDependencies: '@netlify/blobs': 10.1.0 - chokidar: 5.0.0 + chokidar: 4.0.3 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) ioredis: 5.9.2 lru-cache: 11.5.1 - ofetch: 2.0.0-alpha.3 + ofetch: 1.5.1 - unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) + ioredis: 5.9.2 lru-cache: 11.5.1 ofetch: 2.0.0-alpha.3 - unstorage@2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 From e68d04ba38b020168b929b1e7d0af6b89f2daca2 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Wed, 1 Jul 2026 20:24:48 +0300 Subject: [PATCH 05/12] feat(react-start): add isomorphic getCookie/setCookie to root export --- packages/react-start/src/cookies.ts | 43 +++++++++++++++++++++++++++++ packages/react-start/src/index.ts | 1 + 2 files changed, 44 insertions(+) create mode 100644 packages/react-start/src/cookies.ts diff --git a/packages/react-start/src/cookies.ts b/packages/react-start/src/cookies.ts new file mode 100644 index 0000000000..2ada50e138 --- /dev/null +++ b/packages/react-start/src/cookies.ts @@ -0,0 +1,43 @@ +import { createIsomorphicFn } from '@tanstack/start-client-core' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +type GetCookieFn = (name: string) => string | undefined +type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +/** + * Get a cookie value by name. Works on both the server (reads the current + * request's `Cookie` header) and the client (reads `document.cookie`). + * @param name Name of the cookie to get + * @returns Value of the cookie, or `undefined` if not present + * ```ts + * const authorization = getCookie('Authorization') + * ``` + */ +export const getCookie: GetCookieFn = createIsomorphicFn() + .server(getServerCookie) + .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + +/** + * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` + * header on the current response) and the client (writes `document.cookie`). + * @param name Name of the cookie to set + * @param value Value of the cookie to set + * @param options Options for serializing the cookie + * ```ts + * setCookie('Authorization', '1234567') + * ``` + */ +export const setCookie: SetCookieFn = createIsomorphicFn() + .server(setServerCookie) + .client((name: string, value: string, options?: CookieSerializeOptions) => { + document.cookie = serialize(name, value, options) + }) as SetCookieFn diff --git a/packages/react-start/src/index.ts b/packages/react-start/src/index.ts index 6ea839a967..c6c32a9952 100644 --- a/packages/react-start/src/index.ts +++ b/packages/react-start/src/index.ts @@ -1,4 +1,5 @@ export { useServerFn } from './useServerFn' +export { getCookie, setCookie } from './cookies' export * from '@tanstack/start-client-core' // Explicit re-exports shadow `export *` above so these public-API names are // registered on the namespace at link time (via Vite SSR's `defineExport` From a56a6ebab441db5b12f7a83ebcc8741e80bdc59e Mon Sep 17 00:00:00 2001 From: Text Factory Date: Thu, 2 Jul 2026 00:22:29 +0300 Subject: [PATCH 06/12] feat(solid-start): add isomorphic getCookie/setCookie to root export --- packages/solid-start/src/cookies.ts | 43 +++++++++++++++++++++++++++++ packages/solid-start/src/index.ts | 1 + 2 files changed, 44 insertions(+) create mode 100644 packages/solid-start/src/cookies.ts diff --git a/packages/solid-start/src/cookies.ts b/packages/solid-start/src/cookies.ts new file mode 100644 index 0000000000..2ada50e138 --- /dev/null +++ b/packages/solid-start/src/cookies.ts @@ -0,0 +1,43 @@ +import { createIsomorphicFn } from '@tanstack/start-client-core' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +type GetCookieFn = (name: string) => string | undefined +type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +/** + * Get a cookie value by name. Works on both the server (reads the current + * request's `Cookie` header) and the client (reads `document.cookie`). + * @param name Name of the cookie to get + * @returns Value of the cookie, or `undefined` if not present + * ```ts + * const authorization = getCookie('Authorization') + * ``` + */ +export const getCookie: GetCookieFn = createIsomorphicFn() + .server(getServerCookie) + .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + +/** + * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` + * header on the current response) and the client (writes `document.cookie`). + * @param name Name of the cookie to set + * @param value Value of the cookie to set + * @param options Options for serializing the cookie + * ```ts + * setCookie('Authorization', '1234567') + * ``` + */ +export const setCookie: SetCookieFn = createIsomorphicFn() + .server(setServerCookie) + .client((name: string, value: string, options?: CookieSerializeOptions) => { + document.cookie = serialize(name, value, options) + }) as SetCookieFn diff --git a/packages/solid-start/src/index.ts b/packages/solid-start/src/index.ts index bc5560978d..c0772acf46 100644 --- a/packages/solid-start/src/index.ts +++ b/packages/solid-start/src/index.ts @@ -1,4 +1,5 @@ export { useServerFn } from './useServerFn' +export { getCookie, setCookie } from './cookies' export * from '@tanstack/start-client-core' // Explicit re-exports shadow `export *` above so these public-API names are // registered on the namespace at link time (via Vite SSR's `defineExport` From ebf543d51fa46b56338904785b945174d000751e Mon Sep 17 00:00:00 2001 From: Text Factory Date: Thu, 2 Jul 2026 00:28:52 +0300 Subject: [PATCH 07/12] feat(vue-start): add isomorphic getCookie/setCookie to root export --- packages/vue-start/src/cookies.ts | 43 +++++++++++++++++++++++++++++++ packages/vue-start/src/index.ts | 1 + 2 files changed, 44 insertions(+) create mode 100644 packages/vue-start/src/cookies.ts diff --git a/packages/vue-start/src/cookies.ts b/packages/vue-start/src/cookies.ts new file mode 100644 index 0000000000..2ada50e138 --- /dev/null +++ b/packages/vue-start/src/cookies.ts @@ -0,0 +1,43 @@ +import { createIsomorphicFn } from '@tanstack/start-client-core' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +type GetCookieFn = (name: string) => string | undefined +type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +/** + * Get a cookie value by name. Works on both the server (reads the current + * request's `Cookie` header) and the client (reads `document.cookie`). + * @param name Name of the cookie to get + * @returns Value of the cookie, or `undefined` if not present + * ```ts + * const authorization = getCookie('Authorization') + * ``` + */ +export const getCookie: GetCookieFn = createIsomorphicFn() + .server(getServerCookie) + .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + +/** + * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` + * header on the current response) and the client (writes `document.cookie`). + * @param name Name of the cookie to set + * @param value Value of the cookie to set + * @param options Options for serializing the cookie + * ```ts + * setCookie('Authorization', '1234567') + * ``` + */ +export const setCookie: SetCookieFn = createIsomorphicFn() + .server(setServerCookie) + .client((name: string, value: string, options?: CookieSerializeOptions) => { + document.cookie = serialize(name, value, options) + }) as SetCookieFn diff --git a/packages/vue-start/src/index.ts b/packages/vue-start/src/index.ts index 21b3b5b003..14715dc3b5 100644 --- a/packages/vue-start/src/index.ts +++ b/packages/vue-start/src/index.ts @@ -1,4 +1,5 @@ export { useServerFn } from './useServerFn' +export { getCookie, setCookie } from './cookies' export * from '@tanstack/start-client-core' // Explicit re-exports shadow `export *` above so these public-API names are // registered on the namespace at link time (via Vite SSR's `defineExport` From eeb141bc7ee7ffc8865fad94fabfd48b71dd3ade Mon Sep 17 00:00:00 2001 From: Text Factory Date: Thu, 2 Jul 2026 00:44:52 +0300 Subject: [PATCH 08/12] chore: remove .worktrees from .gitignore and delete design documents for isomorphic cookies --- .gitignore | 1 - .../plans/2026-07-01-isomorphic-cookies.md | 443 ------------------ .../2026-07-01-isomorphic-cookies-design.md | 111 ----- 3 files changed, 555 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-01-isomorphic-cookies.md delete mode 100644 docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md diff --git a/.gitignore b/.gitignore index 53129d1368..1699592784 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .vite -.worktrees # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies diff --git a/docs/superpowers/plans/2026-07-01-isomorphic-cookies.md b/docs/superpowers/plans/2026-07-01-isomorphic-cookies.md deleted file mode 100644 index a8c2317d17..0000000000 --- a/docs/superpowers/plans/2026-07-01-isomorphic-cookies.md +++ /dev/null @@ -1,443 +0,0 @@ -# Isomorphic getCookie/setCookie Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Export isomorphic `getCookie(name)` / `setCookie(name, value, options?)` from the root of `@tanstack/react-start`, `@tanstack/solid-start`, and `@tanstack/vue-start` (not `/server`), so the same call works on both client and server. - -**Architecture:** A new `src/cookies.ts` file, identical in content, added to each of the three framework packages. Each uses `createIsomorphicFn` from `@tanstack/start-client-core`: the `.server()` branch delegates to the existing `getCookie`/`setCookie` in `@tanstack/start-server-core`; the `.client()` branch uses `cookie-es`'s `parse`/`serialize` against `document.cookie`. Implemented per-framework (not in the shared `start-client-core`) because `start-server-core` already depends on `start-client-core`, and the reverse would be a circular workspace dependency. - -**Tech Stack:** TypeScript, `createIsomorphicFn` (`@tanstack/start-fn-stubs` via `@tanstack/start-client-core`), `cookie-es`, Vite (package build), publint/attw (package.json validation). - -**Design doc:** `docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md` - ---- - -### Task 1: Add `cookie-es` dependency to the three framework packages - -**Files:** -- Modify: `packages/react-start/package.json:170` -- Modify: `packages/solid-start/package.json:126` -- Modify: `packages/vue-start/package.json:117` - -- [ ] **Step 1: Add `cookie-es` to `packages/react-start/package.json`** - -Current (lines 162-172): -```json - "dependencies": { - "@tanstack/react-router": "workspace:*", - "@tanstack/react-start-client": "workspace:*", - "@tanstack/react-start-rsc": "workspace:*", - "@tanstack/react-start-server": "workspace:*", - "@tanstack/router-utils": "workspace:*", - "@tanstack/start-client-core": "workspace:*", - "@tanstack/start-plugin-core": "workspace:*", - "@tanstack/start-server-core": "workspace:*", - "pathe": "^2.0.3" - }, -``` - -Change to: -```json - "dependencies": { - "@tanstack/react-router": "workspace:*", - "@tanstack/react-start-client": "workspace:*", - "@tanstack/react-start-rsc": "workspace:*", - "@tanstack/react-start-server": "workspace:*", - "@tanstack/router-utils": "workspace:*", - "@tanstack/start-client-core": "workspace:*", - "@tanstack/start-plugin-core": "workspace:*", - "@tanstack/start-server-core": "workspace:*", - "cookie-es": "^3.0.0", - "pathe": "^2.0.3" - }, -``` - -- [ ] **Step 2: Add `cookie-es` to `packages/solid-start/package.json`** - -Current (lines 120-128): -```json - "dependencies": { - "@tanstack/solid-router": "workspace:*", - "@tanstack/solid-start-client": "workspace:*", - "@tanstack/solid-start-server": "workspace:*", - "@tanstack/start-client-core": "workspace:*", - "@tanstack/start-plugin-core": "workspace:*", - "@tanstack/start-server-core": "workspace:*", - "pathe": "^2.0.3" - }, -``` - -Change to: -```json - "dependencies": { - "@tanstack/solid-router": "workspace:*", - "@tanstack/solid-start-client": "workspace:*", - "@tanstack/solid-start-server": "workspace:*", - "@tanstack/start-client-core": "workspace:*", - "@tanstack/start-plugin-core": "workspace:*", - "@tanstack/start-server-core": "workspace:*", - "cookie-es": "^3.0.0", - "pathe": "^2.0.3" - }, -``` - -- [ ] **Step 3: Add `cookie-es` to `packages/vue-start/package.json`** - -Current (lines 114-122): -```json - "dependencies": { - "@tanstack/start-client-core": "workspace:*", - "@tanstack/start-plugin-core": "workspace:*", - "@tanstack/start-server-core": "workspace:*", - "@tanstack/vue-router": "workspace:*", - "@tanstack/vue-start-client": "workspace:*", - "@tanstack/vue-start-server": "workspace:*", - "pathe": "^2.0.3" - }, -``` - -Change to: -```json - "dependencies": { - "@tanstack/start-client-core": "workspace:*", - "@tanstack/start-plugin-core": "workspace:*", - "@tanstack/start-server-core": "workspace:*", - "@tanstack/vue-router": "workspace:*", - "@tanstack/vue-start-client": "workspace:*", - "@tanstack/vue-start-server": "workspace:*", - "cookie-es": "^3.0.0", - "pathe": "^2.0.3" - }, -``` - -- [ ] **Step 4: Install so the lockfile picks up the new dependency** - -Run: `pnpm install` -Expected: exits 0; `pnpm-lock.yaml` is modified to add `cookie-es` under `react-start`, `solid-start`, and `vue-start`. - -- [ ] **Step 5: Commit** - -```bash -git add packages/react-start/package.json packages/solid-start/package.json packages/vue-start/package.json pnpm-lock.yaml -git commit -m "chore: add cookie-es dependency to react-start, solid-start, vue-start" -``` - ---- - -### Task 2: Isomorphic cookies for `react-start` - -**Files:** -- Create: `packages/react-start/src/cookies.ts` -- Modify: `packages/react-start/src/index.ts:1` - -- [ ] **Step 1: Create `packages/react-start/src/cookies.ts`** - -```ts -import { createIsomorphicFn } from '@tanstack/start-client-core' -import { - getCookie as getServerCookie, - setCookie as setServerCookie, -} from '@tanstack/start-server-core' -import { parse, serialize } from 'cookie-es' -import type { CookieSerializeOptions } from 'cookie-es' - -type GetCookieFn = (name: string) => string | undefined -type SetCookieFn = ( - name: string, - value: string, - options?: CookieSerializeOptions, -) => void - -/** - * Get a cookie value by name. Works on both the server (reads the current - * request's `Cookie` header) and the client (reads `document.cookie`). - * @param name Name of the cookie to get - * @returns Value of the cookie, or `undefined` if not present - * ```ts - * const authorization = getCookie('Authorization') - * ``` - */ -export const getCookie: GetCookieFn = createIsomorphicFn() - .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) as GetCookieFn - -/** - * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` - * header on the current response) and the client (writes `document.cookie`). - * @param name Name of the cookie to set - * @param value Value of the cookie to set - * @param options Options for serializing the cookie - * ```ts - * setCookie('Authorization', '1234567') - * ``` - */ -export const setCookie: SetCookieFn = createIsomorphicFn() - .server(setServerCookie) - .client((name: string, value: string, options?: CookieSerializeOptions) => { - document.cookie = serialize(name, value, options) - }) as SetCookieFn -``` - -- [ ] **Step 2: Export from `packages/react-start/src/index.ts`** - -Current (line 1): -```ts -export { useServerFn } from './useServerFn' -``` - -Change to: -```ts -export { useServerFn } from './useServerFn' -export { getCookie, setCookie } from './cookies' -``` - -- [ ] **Step 3: Build the package** - -Run: `pnpm --filter @tanstack/react-start run build` -Expected: exits 0, both `vite build` steps succeed with no TypeScript errors, `dist/esm/index.d.ts` includes `getCookie`/`setCookie`. - -- [ ] **Step 4: Verify package.json/type export validity** - -Run: `pnpm --filter @tanstack/react-start run test:build` -Expected: exits 0 (`publint --strict` and `attw` report no errors). - -- [ ] **Step 5: Commit** - -```bash -git add packages/react-start/src/cookies.ts packages/react-start/src/index.ts -git commit -m "feat(react-start): add isomorphic getCookie/setCookie to root export" -``` - ---- - -### Task 3: Isomorphic cookies for `solid-start` - -**Files:** -- Create: `packages/solid-start/src/cookies.ts` -- Modify: `packages/solid-start/src/index.ts:1` - -- [ ] **Step 1: Create `packages/solid-start/src/cookies.ts`** - -Identical to `packages/react-start/src/cookies.ts` from Task 2, Step 1 (same imports, same code — this package has the same dependencies available): - -```ts -import { createIsomorphicFn } from '@tanstack/start-client-core' -import { - getCookie as getServerCookie, - setCookie as setServerCookie, -} from '@tanstack/start-server-core' -import { parse, serialize } from 'cookie-es' -import type { CookieSerializeOptions } from 'cookie-es' - -type GetCookieFn = (name: string) => string | undefined -type SetCookieFn = ( - name: string, - value: string, - options?: CookieSerializeOptions, -) => void - -/** - * Get a cookie value by name. Works on both the server (reads the current - * request's `Cookie` header) and the client (reads `document.cookie`). - * @param name Name of the cookie to get - * @returns Value of the cookie, or `undefined` if not present - * ```ts - * const authorization = getCookie('Authorization') - * ``` - */ -export const getCookie: GetCookieFn = createIsomorphicFn() - .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) as GetCookieFn - -/** - * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` - * header on the current response) and the client (writes `document.cookie`). - * @param name Name of the cookie to set - * @param value Value of the cookie to set - * @param options Options for serializing the cookie - * ```ts - * setCookie('Authorization', '1234567') - * ``` - */ -export const setCookie: SetCookieFn = createIsomorphicFn() - .server(setServerCookie) - .client((name: string, value: string, options?: CookieSerializeOptions) => { - document.cookie = serialize(name, value, options) - }) as SetCookieFn -``` - -- [ ] **Step 2: Export from `packages/solid-start/src/index.ts`** - -Current (line 1): -```ts -export { useServerFn } from './useServerFn' -``` - -Change to: -```ts -export { useServerFn } from './useServerFn' -export { getCookie, setCookie } from './cookies' -``` - -- [ ] **Step 3: Build the package** - -Run: `pnpm --filter @tanstack/solid-start run build` -Expected: exits 0, both `vite build` steps succeed with no TypeScript errors, `dist/esm/index.d.ts` includes `getCookie`/`setCookie`. - -- [ ] **Step 4: Verify package.json/type export validity** - -Run: `pnpm --filter @tanstack/solid-start run test:build` -Expected: exits 0 (`publint --strict` and `attw` report no errors). - -- [ ] **Step 5: Commit** - -```bash -git add packages/solid-start/src/cookies.ts packages/solid-start/src/index.ts -git commit -m "feat(solid-start): add isomorphic getCookie/setCookie to root export" -``` - ---- - -### Task 4: Isomorphic cookies for `vue-start` - -**Files:** -- Create: `packages/vue-start/src/cookies.ts` -- Modify: `packages/vue-start/src/index.ts:1` - -- [ ] **Step 1: Create `packages/vue-start/src/cookies.ts`** - -Identical to `packages/react-start/src/cookies.ts` from Task 2, Step 1: - -```ts -import { createIsomorphicFn } from '@tanstack/start-client-core' -import { - getCookie as getServerCookie, - setCookie as setServerCookie, -} from '@tanstack/start-server-core' -import { parse, serialize } from 'cookie-es' -import type { CookieSerializeOptions } from 'cookie-es' - -type GetCookieFn = (name: string) => string | undefined -type SetCookieFn = ( - name: string, - value: string, - options?: CookieSerializeOptions, -) => void - -/** - * Get a cookie value by name. Works on both the server (reads the current - * request's `Cookie` header) and the client (reads `document.cookie`). - * @param name Name of the cookie to get - * @returns Value of the cookie, or `undefined` if not present - * ```ts - * const authorization = getCookie('Authorization') - * ``` - */ -export const getCookie: GetCookieFn = createIsomorphicFn() - .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) as GetCookieFn - -/** - * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` - * header on the current response) and the client (writes `document.cookie`). - * @param name Name of the cookie to set - * @param value Value of the cookie to set - * @param options Options for serializing the cookie - * ```ts - * setCookie('Authorization', '1234567') - * ``` - */ -export const setCookie: SetCookieFn = createIsomorphicFn() - .server(setServerCookie) - .client((name: string, value: string, options?: CookieSerializeOptions) => { - document.cookie = serialize(name, value, options) - }) as SetCookieFn -``` - -- [ ] **Step 2: Export from `packages/vue-start/src/index.ts`** - -Current (line 1): -```ts -export { useServerFn } from './useServerFn' -``` - -Change to: -```ts -export { useServerFn } from './useServerFn' -export { getCookie, setCookie } from './cookies' -``` - -- [ ] **Step 3: Build the package** - -Run: `pnpm --filter @tanstack/vue-start run build` -Expected: exits 0, both `vite build` steps succeed with no TypeScript errors, `dist/esm/index.d.ts` includes `getCookie`/`setCookie`. - -- [ ] **Step 4: Verify package.json/type export validity** - -Run: `pnpm --filter @tanstack/vue-start run test:build` -Expected: exits 0 (`publint --strict` and `attw` report no errors). - -- [ ] **Step 5: Commit** - -```bash -git add packages/vue-start/src/cookies.ts packages/vue-start/src/index.ts -git commit -m "feat(vue-start): add isomorphic getCookie/setCookie to root export" -``` - ---- - -### Task 5: Manual smoke check in the `server-functions` e2e app - -**Files:** -- Temporarily modify (revert after verifying): `e2e/react-start/server-functions/src/routes/cookies/set.tsx:1-3` - -The existing fixture at `e2e/react-start/server-functions/src/routes/cookies/set.tsx` already sets cookies on the server (via `setCookie` from `@tanstack/react-start/server`) and reads them back on the client (via the `js-cookie` package, in `RouteComponent`'s `useEffect`). This step temporarily swaps that fixture to use the new isomorphic export end-to-end, to confirm the server-set cookie and a client-set cookie both round-trip correctly through the real dev server. - -- [ ] **Step 1: Temporarily switch the fixture's import** - -In `e2e/react-start/server-functions/src/routes/cookies/set.tsx`, change: - -```ts -import { setCookie } from '@tanstack/react-start/server' -``` - -to: - -```ts -import { getCookie, setCookie } from '@tanstack/react-start' -``` - -- [ ] **Step 2: Add a client-side call to the same `setCookie`/`getCookie` in `RouteComponent`** - -Replace the `useEffect` body in `RouteComponent` (currently reads via `Cookies.get`) with a call that also exercises the isomorphic `setCookie`/`getCookie` from the client: - -```tsx -useEffect(() => { - setCookie(`cookie-5-${expectedCookieValue}`, expectedCookieValue) - const tempCookies: Record = {} - for (let i = 1; i <= 4; i++) { - const key = `cookie-${i}-${expectedCookieValue}` - tempCookies[key] = Cookies.get(key) - } - tempCookies[`cookie-5-${expectedCookieValue}`] = getCookie( - `cookie-5-${expectedCookieValue}`, - ) - setCookiesFromDocument(tempCookies) -}, []) -``` - -- [ ] **Step 3: Run the dev server and check the route in a browser** - -Run: `pnpm --filter tanstack-react-start-e2e-server-functions run dev` - -Visit `/cookies/set?value=test123` in a browser. Expected: the results table shows `cookie-1-test123` through `cookie-4-test123` with value `test123` (server-set, `/server` `setCookie` — unchanged path, confirms no regression), and `cookie-5-test123` also shows `test123` (set via the new isomorphic client `setCookie` and read back via the new isomorphic client `getCookie`, both from `@tanstack/react-start` root — confirms the client branch works). - -Stop the dev server (Ctrl+C) once confirmed. - -- [ ] **Step 4: Revert the temporary fixture changes** - -```bash -git checkout -- e2e/react-start/server-functions/src/routes/cookies/set.tsx -``` - -Expected: `git status` shows no changes in the e2e app (this task never commits anything — it's a manual verification-only step for this plan, not a permanent test addition). diff --git a/docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md b/docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md deleted file mode 100644 index cfff831942..0000000000 --- a/docs/superpowers/specs/2026-07-01-isomorphic-cookies-design.md +++ /dev/null @@ -1,111 +0,0 @@ -# Isomorphic `getCookie` / `setCookie` — Design - -## Problem - -`getCookie` / `setCookie` currently only exist server-side, exported from -`@tanstack/{react,solid,vue}-start/server` (defined in -`packages/start-server-core/src/request-response.ts`). They throw if called -outside an active server request (no `H3Event` in `AsyncLocalStorage`), so -they cannot be called from client code or from isomorphic code paths that run -on both sides. - -## Goal - -Add isomorphic versions of `getCookie(name)` and `setCookie(name, value, options?)` -exported from the **root** of `@tanstack/react-start`, `@tanstack/solid-start`, -and `@tanstack/vue-start` (not `/server`), built with `createIsomorphicFn`, so -the same call works on the server (reads/writes the current request/response) -and on the client (reads/writes `document.cookie`). - -Same function names as the existing server-only versions — no collision, -since the import path differs (`@tanstack/react-start` vs -`@tanstack/react-start/server`). - -Out of scope: `getCookies` (plural, all cookies) and `deleteCookie` stay -server-only for now. `CookieSerializeOptions` is not re-exported as a type, -matching current `/server` behavior. - -## Why not `start-client-core` - -The root export of all three framework packages currently comes from -`export * from '@tanstack/start-client-core'`. That would be the natural home -for a shared isomorphic util, but `start-server-core`'s `package.json` already -declares a dependency on `start-client-core`. Adding the reverse dependency -(`start-client-core` → `start-server-core`, needed to call the existing server -`getCookie`/`setCookie`) would create a circular workspace dependency. - -`packages/react-start`, `packages/solid-start`, and `packages/vue-start` each -already depend on **both** `start-client-core` and `start-server-core` directly, -with no cycle. So the implementation is added once per framework package -(small, ~15-line file, duplicated 3x) instead of shared. This mirrors the -existing precedent where `useServerFn` is already an independent per-framework -file rather than a shared one. - -## Implementation - -New file `src/cookies.ts`, identical content in `packages/react-start`, -`packages/solid-start`, `packages/vue-start`: - -```ts -import { createIsomorphicFn } from '@tanstack/start-client-core' -import { - getCookie as getServerCookie, - setCookie as setServerCookie, -} from '@tanstack/start-server-core' -import { parse, serialize } from 'cookie-es' -import type { CookieSerializeOptions } from 'cookie-es' - -type GetCookieFn = (name: string) => string | undefined -type SetCookieFn = ( - name: string, - value: string, - options?: CookieSerializeOptions, -) => void - -export const getCookie: GetCookieFn = createIsomorphicFn() - .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) as GetCookieFn - -export const setCookie: SetCookieFn = createIsomorphicFn() - .server(setServerCookie) - .client((name: string, value: string, options?: CookieSerializeOptions) => { - document.cookie = serialize(name, value, options) - }) as SetCookieFn -``` - -Exported from each package's root `index.ts` (alongside the existing -`useServerFn` export), e.g. in `packages/react-start/src/index.ts`: - -```ts -export { getCookie, setCookie } from './cookies' -``` - -### Behavior - -- **Server branch** delegates directly to the existing - `start-server-core` implementation — no server-side logic is duplicated. - Same request-scoped behavior as today (throws outside an active request). -- **Client branch** uses `cookie-es`'s `parse`/`serialize` against - `document.cookie`, giving the same option semantics (`path`, `maxAge`, - `expires`, `domain`, `secure`, `sameSite`, etc.) as the server. `httpOnly` is - silently ignored by the browser when set from client-side JS — expected, - unavoidable, and consistent with any client-side cookie write. -- The Start compiler strips whichever branch doesn't match the build target - (documented behavior of `createIsomorphicFn`), so `document.cookie` never - reaches the server bundle and the `start-server-core` import never reaches - the client bundle. - -### New dependency - -Add `cookie-es` (`^3.0.0`, matching `start-server-core`'s existing range) to -the `dependencies` of `react-start`, `solid-start`, and `vue-start` -`package.json`. - -## Testing - -There's no existing unit-test precedent for these small per-framework wrapper -files (`useServerFn`, `createCsrfMiddleware` have none either); verification -relies on typecheck/build passing. Existing e2e cookie fixtures at -`e2e/{react,solid,vue}-start/server-functions/src/routes/cookies/set.tsx` -are a candidate for extension to exercise the new root-level -`getCookie`/`setCookie`, but that's optional and not required for this change. \ No newline at end of file From 681aef9d136b058ad77df804c6cda853490b3f04 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Thu, 2 Jul 2026 01:01:13 +0300 Subject: [PATCH 09/12] fix: warn when setCookie httpOnly option is set in the browser document.cookie cannot honor HttpOnly; a client-side setCookie call with httpOnly: true silently produces a non-HttpOnly cookie. Warn in dev so callers don't mistake this for a protected cookie. --- packages/react-start/src/cookies.ts | 9 +++++++-- packages/solid-start/src/cookies.ts | 9 +++++++-- packages/vue-start/src/cookies.ts | 9 +++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/react-start/src/cookies.ts b/packages/react-start/src/cookies.ts index 2ada50e138..d05740a1da 100644 --- a/packages/react-start/src/cookies.ts +++ b/packages/react-start/src/cookies.ts @@ -24,7 +24,7 @@ type SetCookieFn = ( */ export const getCookie: GetCookieFn = createIsomorphicFn() .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + .client((name: string) => parse(document.cookie)[name]) /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -39,5 +39,10 @@ export const getCookie: GetCookieFn = createIsomorphicFn() export const setCookie: SetCookieFn = createIsomorphicFn() .server(setServerCookie) .client((name: string, value: string, options?: CookieSerializeOptions) => { + if (options?.httpOnly && process.env.NODE_ENV !== 'production') { + console.warn( + '`setCookie` was called with `httpOnly: true` in the browser. `HttpOnly` cannot be set from client-side JavaScript, so this cookie will NOT be HttpOnly-protected.', + ) + } document.cookie = serialize(name, value, options) - }) as SetCookieFn + }) diff --git a/packages/solid-start/src/cookies.ts b/packages/solid-start/src/cookies.ts index 2ada50e138..d05740a1da 100644 --- a/packages/solid-start/src/cookies.ts +++ b/packages/solid-start/src/cookies.ts @@ -24,7 +24,7 @@ type SetCookieFn = ( */ export const getCookie: GetCookieFn = createIsomorphicFn() .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + .client((name: string) => parse(document.cookie)[name]) /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -39,5 +39,10 @@ export const getCookie: GetCookieFn = createIsomorphicFn() export const setCookie: SetCookieFn = createIsomorphicFn() .server(setServerCookie) .client((name: string, value: string, options?: CookieSerializeOptions) => { + if (options?.httpOnly && process.env.NODE_ENV !== 'production') { + console.warn( + '`setCookie` was called with `httpOnly: true` in the browser. `HttpOnly` cannot be set from client-side JavaScript, so this cookie will NOT be HttpOnly-protected.', + ) + } document.cookie = serialize(name, value, options) - }) as SetCookieFn + }) diff --git a/packages/vue-start/src/cookies.ts b/packages/vue-start/src/cookies.ts index 2ada50e138..d05740a1da 100644 --- a/packages/vue-start/src/cookies.ts +++ b/packages/vue-start/src/cookies.ts @@ -24,7 +24,7 @@ type SetCookieFn = ( */ export const getCookie: GetCookieFn = createIsomorphicFn() .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) as GetCookieFn + .client((name: string) => parse(document.cookie)[name]) /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -39,5 +39,10 @@ export const getCookie: GetCookieFn = createIsomorphicFn() export const setCookie: SetCookieFn = createIsomorphicFn() .server(setServerCookie) .client((name: string, value: string, options?: CookieSerializeOptions) => { + if (options?.httpOnly && process.env.NODE_ENV !== 'production') { + console.warn( + '`setCookie` was called with `httpOnly: true` in the browser. `HttpOnly` cannot be set from client-side JavaScript, so this cookie will NOT be HttpOnly-protected.', + ) + } document.cookie = serialize(name, value, options) - }) as SetCookieFn + }) From 03da6e028d83bcfe4fe2b1e413d7ba2a85b6e747 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Thu, 2 Jul 2026 01:21:00 +0300 Subject: [PATCH 10/12] docs: add cookies guide for isomorphic getCookie/setCookie Document the new isomorphic getCookie/setCookie exported from the package root (vs the server-only versions under /server), including the HttpOnly caveat in the browser. Cross-link from Hydration Errors. --- docs/start/config.json | 8 +++ docs/start/framework/react/guide/cookies.md | 65 +++++++++++++++++++ .../framework/react/guide/hydration-errors.md | 2 +- docs/start/framework/solid/guide/cookies.md | 65 +++++++++++++++++++ .../framework/solid/guide/hydration-errors.md | 2 +- 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 docs/start/framework/react/guide/cookies.md create mode 100644 docs/start/framework/solid/guide/cookies.md diff --git a/docs/start/config.json b/docs/start/config.json index e93688eb91..ae401d3f6f 100644 --- a/docs/start/config.json +++ b/docs/start/config.json @@ -101,6 +101,10 @@ "label": "Environment Functions", "to": "framework/react/guide/environment-functions" }, + { + "label": "Cookies", + "to": "framework/react/guide/cookies" + }, { "label": "Middleware", "to": "framework/react/guide/middleware" @@ -238,6 +242,10 @@ "label": "Environment Functions", "to": "framework/solid/guide/environment-functions" }, + { + "label": "Cookies", + "to": "framework/solid/guide/cookies" + }, { "label": "Middleware", "to": "framework/solid/guide/middleware" diff --git a/docs/start/framework/react/guide/cookies.md b/docs/start/framework/react/guide/cookies.md new file mode 100644 index 0000000000..517d883d59 --- /dev/null +++ b/docs/start/framework/react/guide/cookies.md @@ -0,0 +1,65 @@ +--- +id: cookies +title: Cookies +--- + +Start ships two ways to read and write cookies: + +- **Server-only**: `getCookie`/`setCookie`/`getCookies`/`deleteCookie` from `@tanstack/react-start/server`. These only work inside an active server request (a server function, request middleware, or a server route) and throw otherwise. +- **Isomorphic**: `getCookie`/`setCookie` from the root of `@tanstack/react-start`. The same call works on the server _and_ in the browser — pick this when a component or a shared helper needs to read/write a cookie regardless of which side it renders on. + +## Isomorphic `getCookie`/`setCookie` + +```tsx +import { getCookie, setCookie } from '@tanstack/react-start' +``` + +These are built with [`createIsomorphicFn`](./environment-functions): on the server they delegate to the same request-scoped cookie handling as the `/server` versions; in the browser they read/write `document.cookie` directly. + +```tsx +import * as React from 'react' +import { getCookie, setCookie } from '@tanstack/react-start' + +export const getServerNow = createServerFn().handler(async () => { + // Runs on the server: reads the incoming request's `Cookie` header. + const theme = getCookie('theme') ?? 'light' + return theme +}) + +function ThemeToggle() { + const [theme, setTheme] = React.useState(() => getCookie('theme') ?? 'light') + + return ( + + ) +} +``` + +## `HttpOnly` only works on the server + +`setCookie`'s `options` accept the same [`CookieSerializeOptions`](https://github.com/unjs/cookie-es) as the server-only version, including `httpOnly`. But `HttpOnly` is a protection against client-side JavaScript reading the cookie — so it can only be set from the server. + +If `setCookie` runs in the browser (the client branch) with `httpOnly: true`, the browser doesn't just ignore the flag — it silently discards the entire cookie write. `setCookie` logs a `console.warn` in development to help catch this, but there's no way to recover the write: if a cookie needs `HttpOnly`, set it from a server function or request middleware, not from client code. + +```tsx +// ❌ Runs in the browser: the cookie is never actually set. +setCookie('session', token, { httpOnly: true }) + +// ✅ Runs on the server: `HttpOnly` is honored. +export const login = createServerFn().handler(async () => { + setCookie('session', token, { httpOnly: true }) +}) +``` + +## When to use the server-only versions instead + +Reach for `getCookie`/`setCookie`/`getCookies`/`deleteCookie` from `@tanstack/react-start/server` when the code only ever runs on the server (a server function handler, request middleware, or a server route) and you don't need it to also work in the browser — for example, reading a locale cookie inside request middleware, as shown in [Hydration Errors](./hydration-errors). Calling the server-only versions from client code throws instead of silently doing something unexpected. diff --git a/docs/start/framework/react/guide/hydration-errors.md b/docs/start/framework/react/guide/hydration-errors.md index fe0596684a..3342b1ccdc 100644 --- a/docs/start/framework/react/guide/hydration-errors.md +++ b/docs/start/framework/react/guide/hydration-errors.md @@ -133,4 +133,4 @@ export const Route = createFileRoute('/unstable')({ - **Use Selective SSR** when server HTML cannot be stable - **Avoid blind suppression**; use `suppressHydrationWarning` sparingly -See also: [Execution Model](./execution-model.md), [Code Execution Patterns](./code-execution-patterns.md), [Selective SSR](./selective-ssr.md), [Server Functions](./server-functions.md) +See also: [Execution Model](./execution-model.md), [Code Execution Patterns](./code-execution-patterns.md), [Selective SSR](./selective-ssr.md), [Server Functions](./server-functions.md), [Cookies](./cookies.md) diff --git a/docs/start/framework/solid/guide/cookies.md b/docs/start/framework/solid/guide/cookies.md new file mode 100644 index 0000000000..6c0ef1a25b --- /dev/null +++ b/docs/start/framework/solid/guide/cookies.md @@ -0,0 +1,65 @@ +--- +id: cookies +title: Cookies +--- + +Start ships two ways to read and write cookies: + +- **Server-only**: `getCookie`/`setCookie`/`getCookies`/`deleteCookie` from `@tanstack/solid-start/server`. These only work inside an active server request (a server function, request middleware, or a server route) and throw otherwise. +- **Isomorphic**: `getCookie`/`setCookie` from the root of `@tanstack/solid-start`. The same call works on the server _and_ in the browser — pick this when a component or a shared helper needs to read/write a cookie regardless of which side it renders on. + +## Isomorphic `getCookie`/`setCookie` + +```tsx +import { getCookie, setCookie } from '@tanstack/solid-start' +``` + +These are built with [`createIsomorphicFn`](./environment-functions): on the server they delegate to the same request-scoped cookie handling as the `/server` versions; in the browser they read/write `document.cookie` directly. + +```tsx +import { createSignal } from 'solid-js' +import { getCookie, setCookie } from '@tanstack/solid-start' + +export const getServerNow = createServerFn().handler(async () => { + // Runs on the server: reads the incoming request's `Cookie` header. + const theme = getCookie('theme') ?? 'light' + return theme +}) + +function ThemeToggle() { + const [theme, setTheme] = createSignal(getCookie('theme') ?? 'light') + + return ( + + ) +} +``` + +## `HttpOnly` only works on the server + +`setCookie`'s `options` accept the same [`CookieSerializeOptions`](https://github.com/unjs/cookie-es) as the server-only version, including `httpOnly`. But `HttpOnly` is a protection against client-side JavaScript reading the cookie — so it can only be set from the server. + +If `setCookie` runs in the browser (the client branch) with `httpOnly: true`, the browser doesn't just ignore the flag — it silently discards the entire cookie write. `setCookie` logs a `console.warn` in development to help catch this, but there's no way to recover the write: if a cookie needs `HttpOnly`, set it from a server function or request middleware, not from client code. + +```tsx +// ❌ Runs in the browser: the cookie is never actually set. +setCookie('session', token, { httpOnly: true }) + +// ✅ Runs on the server: `HttpOnly` is honored. +export const login = createServerFn().handler(async () => { + setCookie('session', token, { httpOnly: true }) +}) +``` + +## When to use the server-only versions instead + +Reach for `getCookie`/`setCookie`/`getCookies`/`deleteCookie` from `@tanstack/solid-start/server` when the code only ever runs on the server (a server function handler, request middleware, or a server route) and you don't need it to also work in the browser — for example, reading a locale cookie inside request middleware, as shown in [Hydration Errors](./hydration-errors). Calling the server-only versions from client code throws instead of silently doing something unexpected. diff --git a/docs/start/framework/solid/guide/hydration-errors.md b/docs/start/framework/solid/guide/hydration-errors.md index 75e44280c9..2a49fdeaee 100644 --- a/docs/start/framework/solid/guide/hydration-errors.md +++ b/docs/start/framework/solid/guide/hydration-errors.md @@ -126,4 +126,4 @@ export const Route = createFileRoute('/unstable')({ - **Use ``/``** for inherently dynamic UI - **Use Selective SSR** when server HTML cannot be stable -See also: [Execution Model](./execution-model.md), [Code Execution Patterns](./code-execution-patterns.md), [Selective SSR](./selective-ssr.md), [Server Functions](./server-functions.md) +See also: [Execution Model](./execution-model.md), [Code Execution Patterns](./code-execution-patterns.md), [Selective SSR](./selective-ssr.md), [Server Functions](./server-functions.md), [Cookies](./cookies.md) From 1bbe9cb319c27d15f8d2276b529876e8ecb18ec6 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Thu, 2 Jul 2026 01:21:20 +0300 Subject: [PATCH 11/12] test: add unit tests for isomorphic getCookie/setCookie Add tests/cookies.test.ts to react-start, solid-start, and vue-start, and wire up a test:unit script (vitest) matching start-client-core's convention. Since createIsomorphicFn's uncompiled runtime fallback always resolves to the .server() implementation once one is registered, the .client() branch is unreachable through the exported getCookie/setCookie outside of a Start-compiled bundle. Extract the client implementations into named getClientCookie/setClientCookie functions (not part of the package's public entry) so they can be tested directly. Also corrects the httpOnly warning message: per the WHATWG cookie spec, browsers discard the entire cookie (not just the HttpOnly attribute) when set via document.cookie with HttpOnly present, which the new tests caught. --- packages/react-start/package.json | 4 +- packages/react-start/src/cookies.ts | 33 ++++-- packages/react-start/tests/cookies.test.ts | 113 +++++++++++++++++++++ packages/solid-start/package.json | 4 +- packages/solid-start/src/cookies.ts | 33 ++++-- packages/solid-start/tests/cookies.test.ts | 113 +++++++++++++++++++++ packages/vue-start/package.json | 4 +- packages/vue-start/src/cookies.ts | 33 ++++-- packages/vue-start/tests/cookies.test.ts | 113 +++++++++++++++++++++ 9 files changed, 420 insertions(+), 30 deletions(-) create mode 100644 packages/react-start/tests/cookies.test.ts create mode 100644 packages/solid-start/tests/cookies.test.ts create mode 100644 packages/vue-start/tests/cookies.test.ts diff --git a/packages/react-start/package.json b/packages/react-start/package.json index 0a61e9268f..b0aa318f6d 100644 --- a/packages/react-start/package.json +++ b/packages/react-start/package.json @@ -25,7 +25,9 @@ ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", - "test": "pnpm test:build", + "test": "pnpm test:build && pnpm test:unit", + "test:unit": "vitest", + "test:unit:dev": "vitest --watch", "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", "build": "vite build && vite build -c vite.config.server-entry.ts" }, diff --git a/packages/react-start/src/cookies.ts b/packages/react-start/src/cookies.ts index d05740a1da..42e5257a29 100644 --- a/packages/react-start/src/cookies.ts +++ b/packages/react-start/src/cookies.ts @@ -13,6 +13,28 @@ type SetCookieFn = ( options?: CookieSerializeOptions, ) => void +// Exported (but not part of the package's public entry) so they can be unit +// tested directly, since `createIsomorphicFn`'s uncompiled runtime fallback +// always resolves to the `.server()` implementation once one is registered, +// making the `.client()` branch unreachable through `getCookie`/`setCookie` +// outside of a Start-compiled bundle. +export function getClientCookie(name: string): string | undefined { + return parse(document.cookie)[name] +} + +export function setClientCookie( + name: string, + value: string, + options?: CookieSerializeOptions, +): void { + if (options?.httpOnly && process.env.NODE_ENV !== 'production') { + console.warn( + '`setCookie` was called with `httpOnly: true` in the browser. Browsers silently discard cookies written via `document.cookie` when `HttpOnly` is set, so this cookie will NOT be set.', + ) + } + document.cookie = serialize(name, value, options) +} + /** * Get a cookie value by name. Works on both the server (reads the current * request's `Cookie` header) and the client (reads `document.cookie`). @@ -24,7 +46,7 @@ type SetCookieFn = ( */ export const getCookie: GetCookieFn = createIsomorphicFn() .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) + .client(getClientCookie) /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -38,11 +60,4 @@ export const getCookie: GetCookieFn = createIsomorphicFn() */ export const setCookie: SetCookieFn = createIsomorphicFn() .server(setServerCookie) - .client((name: string, value: string, options?: CookieSerializeOptions) => { - if (options?.httpOnly && process.env.NODE_ENV !== 'production') { - console.warn( - '`setCookie` was called with `httpOnly: true` in the browser. `HttpOnly` cannot be set from client-side JavaScript, so this cookie will NOT be HttpOnly-protected.', - ) - } - document.cookie = serialize(name, value, options) - }) + .client(setClientCookie) diff --git a/packages/react-start/tests/cookies.test.ts b/packages/react-start/tests/cookies.test.ts new file mode 100644 index 0000000000..5076f8fa57 --- /dev/null +++ b/packages/react-start/tests/cookies.test.ts @@ -0,0 +1,113 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const getServerCookieMock = vi.fn<(name: string) => string | undefined>() +const setServerCookieMock = vi.fn() + +vi.mock('@tanstack/start-server-core', () => ({ + getCookie: getServerCookieMock, + setCookie: setServerCookieMock, +})) + +const { getCookie, setCookie, getClientCookie, setClientCookie } = + await import('../src/cookies') + +function clearDocumentCookies() { + document.cookie.split(';').forEach((cookie) => { + const name = cookie.split('=')[0]?.trim() + if (name) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + } + }) +} + +describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', () => { + beforeEach(() => { + getServerCookieMock.mockReset() + setServerCookieMock.mockReset() + }) + + // Outside of a Start-compiled bundle, createIsomorphicFn's runtime fallback + // always resolves to the `.server()` implementation once one is + // registered (see packages/start-fn-stubs/src/createIsomorphicFn.ts), so + // calling the exported getCookie/setCookie here exercises the server + // delegation, not the client branch. + it('delegates getCookie to start-server-core', () => { + getServerCookieMock.mockReturnValue('server-value') + + expect(getCookie('token')).toBe('server-value') + expect(getServerCookieMock).toHaveBeenCalledExactlyOnceWith('token') + }) + + it('delegates setCookie to start-server-core', () => { + setCookie('token', 'abc123', { path: '/' }) + + expect(setServerCookieMock).toHaveBeenCalledExactlyOnceWith( + 'token', + 'abc123', + { path: '/' }, + ) + }) +}) + +describe('getClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + }) + + it('reads a cookie value from document.cookie', () => { + document.cookie = 'token=abc123' + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('returns undefined for a cookie that is not present', () => { + expect(getClientCookie('missing')).toBeUndefined() + }) +}) + +describe('setClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + vi.unstubAllEnvs() + vi.restoreAllMocks() + }) + + it('writes the cookie to document.cookie', () => { + setClientCookie('token', 'abc123', { path: '/' }) + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('warns in development when httpOnly is set, since browsers silently discard the cookie', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining('HttpOnly'), + ) + // Per the WHATWG cookie spec, browsers discard the entire cookie (not + // just the HttpOnly attribute) when it's set via `document.cookie` with + // `HttpOnly` present — this is real behavior, not a quirk of the warning. + expect(getClientCookie('token')).toBeUndefined() + }) + + it('does not warn when httpOnly is not set', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123') + + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('does not warn in production even when httpOnly is set', () => { + vi.stubEnv('NODE_ENV', 'production') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index 8689a791a3..4112ac504f 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -25,7 +25,9 @@ ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", - "test": "pnpm test:build", + "test": "pnpm test:build && pnpm test:unit", + "test:unit": "vitest", + "test:unit:dev": "vitest --watch", "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", "build": "vite build && vite build -c vite.config.server-entry.ts" }, diff --git a/packages/solid-start/src/cookies.ts b/packages/solid-start/src/cookies.ts index d05740a1da..42e5257a29 100644 --- a/packages/solid-start/src/cookies.ts +++ b/packages/solid-start/src/cookies.ts @@ -13,6 +13,28 @@ type SetCookieFn = ( options?: CookieSerializeOptions, ) => void +// Exported (but not part of the package's public entry) so they can be unit +// tested directly, since `createIsomorphicFn`'s uncompiled runtime fallback +// always resolves to the `.server()` implementation once one is registered, +// making the `.client()` branch unreachable through `getCookie`/`setCookie` +// outside of a Start-compiled bundle. +export function getClientCookie(name: string): string | undefined { + return parse(document.cookie)[name] +} + +export function setClientCookie( + name: string, + value: string, + options?: CookieSerializeOptions, +): void { + if (options?.httpOnly && process.env.NODE_ENV !== 'production') { + console.warn( + '`setCookie` was called with `httpOnly: true` in the browser. Browsers silently discard cookies written via `document.cookie` when `HttpOnly` is set, so this cookie will NOT be set.', + ) + } + document.cookie = serialize(name, value, options) +} + /** * Get a cookie value by name. Works on both the server (reads the current * request's `Cookie` header) and the client (reads `document.cookie`). @@ -24,7 +46,7 @@ type SetCookieFn = ( */ export const getCookie: GetCookieFn = createIsomorphicFn() .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) + .client(getClientCookie) /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -38,11 +60,4 @@ export const getCookie: GetCookieFn = createIsomorphicFn() */ export const setCookie: SetCookieFn = createIsomorphicFn() .server(setServerCookie) - .client((name: string, value: string, options?: CookieSerializeOptions) => { - if (options?.httpOnly && process.env.NODE_ENV !== 'production') { - console.warn( - '`setCookie` was called with `httpOnly: true` in the browser. `HttpOnly` cannot be set from client-side JavaScript, so this cookie will NOT be HttpOnly-protected.', - ) - } - document.cookie = serialize(name, value, options) - }) + .client(setClientCookie) diff --git a/packages/solid-start/tests/cookies.test.ts b/packages/solid-start/tests/cookies.test.ts new file mode 100644 index 0000000000..5076f8fa57 --- /dev/null +++ b/packages/solid-start/tests/cookies.test.ts @@ -0,0 +1,113 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const getServerCookieMock = vi.fn<(name: string) => string | undefined>() +const setServerCookieMock = vi.fn() + +vi.mock('@tanstack/start-server-core', () => ({ + getCookie: getServerCookieMock, + setCookie: setServerCookieMock, +})) + +const { getCookie, setCookie, getClientCookie, setClientCookie } = + await import('../src/cookies') + +function clearDocumentCookies() { + document.cookie.split(';').forEach((cookie) => { + const name = cookie.split('=')[0]?.trim() + if (name) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + } + }) +} + +describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', () => { + beforeEach(() => { + getServerCookieMock.mockReset() + setServerCookieMock.mockReset() + }) + + // Outside of a Start-compiled bundle, createIsomorphicFn's runtime fallback + // always resolves to the `.server()` implementation once one is + // registered (see packages/start-fn-stubs/src/createIsomorphicFn.ts), so + // calling the exported getCookie/setCookie here exercises the server + // delegation, not the client branch. + it('delegates getCookie to start-server-core', () => { + getServerCookieMock.mockReturnValue('server-value') + + expect(getCookie('token')).toBe('server-value') + expect(getServerCookieMock).toHaveBeenCalledExactlyOnceWith('token') + }) + + it('delegates setCookie to start-server-core', () => { + setCookie('token', 'abc123', { path: '/' }) + + expect(setServerCookieMock).toHaveBeenCalledExactlyOnceWith( + 'token', + 'abc123', + { path: '/' }, + ) + }) +}) + +describe('getClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + }) + + it('reads a cookie value from document.cookie', () => { + document.cookie = 'token=abc123' + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('returns undefined for a cookie that is not present', () => { + expect(getClientCookie('missing')).toBeUndefined() + }) +}) + +describe('setClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + vi.unstubAllEnvs() + vi.restoreAllMocks() + }) + + it('writes the cookie to document.cookie', () => { + setClientCookie('token', 'abc123', { path: '/' }) + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('warns in development when httpOnly is set, since browsers silently discard the cookie', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining('HttpOnly'), + ) + // Per the WHATWG cookie spec, browsers discard the entire cookie (not + // just the HttpOnly attribute) when it's set via `document.cookie` with + // `HttpOnly` present — this is real behavior, not a quirk of the warning. + expect(getClientCookie('token')).toBeUndefined() + }) + + it('does not warn when httpOnly is not set', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123') + + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('does not warn in production even when httpOnly is set', () => { + vi.stubEnv('NODE_ENV', 'production') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/vue-start/package.json b/packages/vue-start/package.json index ff786c1fae..ac816c6b21 100644 --- a/packages/vue-start/package.json +++ b/packages/vue-start/package.json @@ -25,7 +25,9 @@ ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", - "test": "pnpm test:build", + "test": "pnpm test:build && pnpm test:unit", + "test:unit": "vitest", + "test:unit:dev": "vitest --watch", "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", "build": "vite build && vite build -c vite.config.server-entry.ts" }, diff --git a/packages/vue-start/src/cookies.ts b/packages/vue-start/src/cookies.ts index d05740a1da..42e5257a29 100644 --- a/packages/vue-start/src/cookies.ts +++ b/packages/vue-start/src/cookies.ts @@ -13,6 +13,28 @@ type SetCookieFn = ( options?: CookieSerializeOptions, ) => void +// Exported (but not part of the package's public entry) so they can be unit +// tested directly, since `createIsomorphicFn`'s uncompiled runtime fallback +// always resolves to the `.server()` implementation once one is registered, +// making the `.client()` branch unreachable through `getCookie`/`setCookie` +// outside of a Start-compiled bundle. +export function getClientCookie(name: string): string | undefined { + return parse(document.cookie)[name] +} + +export function setClientCookie( + name: string, + value: string, + options?: CookieSerializeOptions, +): void { + if (options?.httpOnly && process.env.NODE_ENV !== 'production') { + console.warn( + '`setCookie` was called with `httpOnly: true` in the browser. Browsers silently discard cookies written via `document.cookie` when `HttpOnly` is set, so this cookie will NOT be set.', + ) + } + document.cookie = serialize(name, value, options) +} + /** * Get a cookie value by name. Works on both the server (reads the current * request's `Cookie` header) and the client (reads `document.cookie`). @@ -24,7 +46,7 @@ type SetCookieFn = ( */ export const getCookie: GetCookieFn = createIsomorphicFn() .server(getServerCookie) - .client((name: string) => parse(document.cookie)[name]) + .client(getClientCookie) /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -38,11 +60,4 @@ export const getCookie: GetCookieFn = createIsomorphicFn() */ export const setCookie: SetCookieFn = createIsomorphicFn() .server(setServerCookie) - .client((name: string, value: string, options?: CookieSerializeOptions) => { - if (options?.httpOnly && process.env.NODE_ENV !== 'production') { - console.warn( - '`setCookie` was called with `httpOnly: true` in the browser. `HttpOnly` cannot be set from client-side JavaScript, so this cookie will NOT be HttpOnly-protected.', - ) - } - document.cookie = serialize(name, value, options) - }) + .client(setClientCookie) diff --git a/packages/vue-start/tests/cookies.test.ts b/packages/vue-start/tests/cookies.test.ts new file mode 100644 index 0000000000..5076f8fa57 --- /dev/null +++ b/packages/vue-start/tests/cookies.test.ts @@ -0,0 +1,113 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const getServerCookieMock = vi.fn<(name: string) => string | undefined>() +const setServerCookieMock = vi.fn() + +vi.mock('@tanstack/start-server-core', () => ({ + getCookie: getServerCookieMock, + setCookie: setServerCookieMock, +})) + +const { getCookie, setCookie, getClientCookie, setClientCookie } = + await import('../src/cookies') + +function clearDocumentCookies() { + document.cookie.split(';').forEach((cookie) => { + const name = cookie.split('=')[0]?.trim() + if (name) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + } + }) +} + +describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', () => { + beforeEach(() => { + getServerCookieMock.mockReset() + setServerCookieMock.mockReset() + }) + + // Outside of a Start-compiled bundle, createIsomorphicFn's runtime fallback + // always resolves to the `.server()` implementation once one is + // registered (see packages/start-fn-stubs/src/createIsomorphicFn.ts), so + // calling the exported getCookie/setCookie here exercises the server + // delegation, not the client branch. + it('delegates getCookie to start-server-core', () => { + getServerCookieMock.mockReturnValue('server-value') + + expect(getCookie('token')).toBe('server-value') + expect(getServerCookieMock).toHaveBeenCalledExactlyOnceWith('token') + }) + + it('delegates setCookie to start-server-core', () => { + setCookie('token', 'abc123', { path: '/' }) + + expect(setServerCookieMock).toHaveBeenCalledExactlyOnceWith( + 'token', + 'abc123', + { path: '/' }, + ) + }) +}) + +describe('getClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + }) + + it('reads a cookie value from document.cookie', () => { + document.cookie = 'token=abc123' + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('returns undefined for a cookie that is not present', () => { + expect(getClientCookie('missing')).toBeUndefined() + }) +}) + +describe('setClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + vi.unstubAllEnvs() + vi.restoreAllMocks() + }) + + it('writes the cookie to document.cookie', () => { + setClientCookie('token', 'abc123', { path: '/' }) + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('warns in development when httpOnly is set, since browsers silently discard the cookie', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining('HttpOnly'), + ) + // Per the WHATWG cookie spec, browsers discard the entire cookie (not + // just the HttpOnly attribute) when it's set via `document.cookie` with + // `HttpOnly` present — this is real behavior, not a quirk of the warning. + expect(getClientCookie('token')).toBeUndefined() + }) + + it('does not warn when httpOnly is not set', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123') + + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('does not warn in production even when httpOnly is set', () => { + vi.stubEnv('NODE_ENV', 'production') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).not.toHaveBeenCalled() + }) +}) From bc6d21e12c5b836126a9dd9d75d3cdbec65b9e51 Mon Sep 17 00:00:00 2001 From: Text Factory Date: Thu, 2 Jul 2026 01:46:16 +0300 Subject: [PATCH 12/12] refactor: move isomorphic cookie logic into start-client-core Extract the client-side cookie logic (parsing/serializing document.cookie, the httpOnly warning) and the createIsomorphicFn wiring into a new createCookieFns(server) factory in start-client-core, exposed via a new ./cookies subpath (not re-exported from the package root, so it doesn't leak into react-start/solid-start/vue-start's public API). The server implementation is injected as a parameter rather than imported directly, since start-server-core already depends on start-client-core and a static import the other way would be a circular workspace dependency. Each framework's cookies.ts shrinks to a ~25-line file that calls createCookieFns with its own start-server-core getCookie/setCookie. The cookie-es dependency and the client-logic unit tests move to start-client-core accordingly; each framework's remaining test just verifies its own getCookie/setCookie delegate to the right start-server-core functions. --- packages/react-start/package.json | 1 - packages/react-start/src/cookies.ts | 44 ++----- packages/react-start/tests/cookies.test.ts | 88 ++----------- packages/solid-start/package.json | 1 - packages/solid-start/src/cookies.ts | 44 ++----- packages/solid-start/tests/cookies.test.ts | 88 ++----------- packages/start-client-core/package.json | 7 ++ packages/start-client-core/src/cookies.ts | 58 +++++++++ .../start-client-core/tests/cookies.test.ts | 119 ++++++++++++++++++ packages/start-client-core/vite.config.ts | 1 + packages/vue-start/package.json | 1 - packages/vue-start/src/cookies.ts | 44 ++----- packages/vue-start/tests/cookies.test.ts | 88 ++----------- pnpm-lock.yaml | 12 +- 14 files changed, 230 insertions(+), 366 deletions(-) create mode 100644 packages/start-client-core/src/cookies.ts create mode 100644 packages/start-client-core/tests/cookies.test.ts diff --git a/packages/react-start/package.json b/packages/react-start/package.json index b0aa318f6d..a2e192c155 100644 --- a/packages/react-start/package.json +++ b/packages/react-start/package.json @@ -170,7 +170,6 @@ "@tanstack/start-client-core": "workspace:*", "@tanstack/start-plugin-core": "workspace:*", "@tanstack/start-server-core": "workspace:*", - "cookie-es": "^3.0.0", "pathe": "^2.0.3" }, "peerDependencies": { diff --git a/packages/react-start/src/cookies.ts b/packages/react-start/src/cookies.ts index 42e5257a29..037e04b5d6 100644 --- a/packages/react-start/src/cookies.ts +++ b/packages/react-start/src/cookies.ts @@ -1,39 +1,13 @@ -import { createIsomorphicFn } from '@tanstack/start-client-core' +import { createCookieFns } from '@tanstack/start-client-core/cookies' import { getCookie as getServerCookie, setCookie as setServerCookie, } from '@tanstack/start-server-core' -import { parse, serialize } from 'cookie-es' -import type { CookieSerializeOptions } from 'cookie-es' -type GetCookieFn = (name: string) => string | undefined -type SetCookieFn = ( - name: string, - value: string, - options?: CookieSerializeOptions, -) => void - -// Exported (but not part of the package's public entry) so they can be unit -// tested directly, since `createIsomorphicFn`'s uncompiled runtime fallback -// always resolves to the `.server()` implementation once one is registered, -// making the `.client()` branch unreachable through `getCookie`/`setCookie` -// outside of a Start-compiled bundle. -export function getClientCookie(name: string): string | undefined { - return parse(document.cookie)[name] -} - -export function setClientCookie( - name: string, - value: string, - options?: CookieSerializeOptions, -): void { - if (options?.httpOnly && process.env.NODE_ENV !== 'production') { - console.warn( - '`setCookie` was called with `httpOnly: true` in the browser. Browsers silently discard cookies written via `document.cookie` when `HttpOnly` is set, so this cookie will NOT be set.', - ) - } - document.cookie = serialize(name, value, options) -} +const cookieFns = createCookieFns({ + getCookie: getServerCookie, + setCookie: setServerCookie, +}) /** * Get a cookie value by name. Works on both the server (reads the current @@ -44,9 +18,7 @@ export function setClientCookie( * const authorization = getCookie('Authorization') * ``` */ -export const getCookie: GetCookieFn = createIsomorphicFn() - .server(getServerCookie) - .client(getClientCookie) +export const getCookie = cookieFns.getCookie /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -58,6 +30,4 @@ export const getCookie: GetCookieFn = createIsomorphicFn() * setCookie('Authorization', '1234567') * ``` */ -export const setCookie: SetCookieFn = createIsomorphicFn() - .server(setServerCookie) - .client(setClientCookie) +export const setCookie = cookieFns.setCookie diff --git a/packages/react-start/tests/cookies.test.ts b/packages/react-start/tests/cookies.test.ts index 5076f8fa57..2e1dcc2e00 100644 --- a/packages/react-start/tests/cookies.test.ts +++ b/packages/react-start/tests/cookies.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' const getServerCookieMock = vi.fn<(name: string) => string | undefined>() const setServerCookieMock = vi.fn() @@ -8,29 +8,18 @@ vi.mock('@tanstack/start-server-core', () => ({ setCookie: setServerCookieMock, })) -const { getCookie, setCookie, getClientCookie, setClientCookie } = - await import('../src/cookies') +const { getCookie, setCookie } = await import('../src/cookies') -function clearDocumentCookies() { - document.cookie.split(';').forEach((cookie) => { - const name = cookie.split('=')[0]?.trim() - if (name) { - document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` - } - }) -} - -describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', () => { +// The cookie logic itself (client-side parsing/serialization, the httpOnly +// warning, and createCookieFns' delegation behavior) is covered by +// start-client-core's own tests. This only verifies that this package wires +// its getCookie/setCookie up to the right start-server-core functions. +describe('getCookie/setCookie', () => { beforeEach(() => { getServerCookieMock.mockReset() setServerCookieMock.mockReset() }) - // Outside of a Start-compiled bundle, createIsomorphicFn's runtime fallback - // always resolves to the `.server()` implementation once one is - // registered (see packages/start-fn-stubs/src/createIsomorphicFn.ts), so - // calling the exported getCookie/setCookie here exercises the server - // delegation, not the client branch. it('delegates getCookie to start-server-core', () => { getServerCookieMock.mockReturnValue('server-value') @@ -48,66 +37,3 @@ describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', ) }) }) - -describe('getClientCookie', () => { - afterEach(() => { - clearDocumentCookies() - }) - - it('reads a cookie value from document.cookie', () => { - document.cookie = 'token=abc123' - - expect(getClientCookie('token')).toBe('abc123') - }) - - it('returns undefined for a cookie that is not present', () => { - expect(getClientCookie('missing')).toBeUndefined() - }) -}) - -describe('setClientCookie', () => { - afterEach(() => { - clearDocumentCookies() - vi.unstubAllEnvs() - vi.restoreAllMocks() - }) - - it('writes the cookie to document.cookie', () => { - setClientCookie('token', 'abc123', { path: '/' }) - - expect(getClientCookie('token')).toBe('abc123') - }) - - it('warns in development when httpOnly is set, since browsers silently discard the cookie', () => { - vi.stubEnv('NODE_ENV', 'development') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123', { httpOnly: true }) - - expect(warnSpy).toHaveBeenCalledExactlyOnceWith( - expect.stringContaining('HttpOnly'), - ) - // Per the WHATWG cookie spec, browsers discard the entire cookie (not - // just the HttpOnly attribute) when it's set via `document.cookie` with - // `HttpOnly` present — this is real behavior, not a quirk of the warning. - expect(getClientCookie('token')).toBeUndefined() - }) - - it('does not warn when httpOnly is not set', () => { - vi.stubEnv('NODE_ENV', 'development') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123') - - expect(warnSpy).not.toHaveBeenCalled() - }) - - it('does not warn in production even when httpOnly is set', () => { - vi.stubEnv('NODE_ENV', 'production') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123', { httpOnly: true }) - - expect(warnSpy).not.toHaveBeenCalled() - }) -}) diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index 4112ac504f..20d0b576a9 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -126,7 +126,6 @@ "@tanstack/start-client-core": "workspace:*", "@tanstack/start-plugin-core": "workspace:*", "@tanstack/start-server-core": "workspace:*", - "cookie-es": "^3.0.0", "pathe": "^2.0.3" }, "devDependencies": { diff --git a/packages/solid-start/src/cookies.ts b/packages/solid-start/src/cookies.ts index 42e5257a29..037e04b5d6 100644 --- a/packages/solid-start/src/cookies.ts +++ b/packages/solid-start/src/cookies.ts @@ -1,39 +1,13 @@ -import { createIsomorphicFn } from '@tanstack/start-client-core' +import { createCookieFns } from '@tanstack/start-client-core/cookies' import { getCookie as getServerCookie, setCookie as setServerCookie, } from '@tanstack/start-server-core' -import { parse, serialize } from 'cookie-es' -import type { CookieSerializeOptions } from 'cookie-es' -type GetCookieFn = (name: string) => string | undefined -type SetCookieFn = ( - name: string, - value: string, - options?: CookieSerializeOptions, -) => void - -// Exported (but not part of the package's public entry) so they can be unit -// tested directly, since `createIsomorphicFn`'s uncompiled runtime fallback -// always resolves to the `.server()` implementation once one is registered, -// making the `.client()` branch unreachable through `getCookie`/`setCookie` -// outside of a Start-compiled bundle. -export function getClientCookie(name: string): string | undefined { - return parse(document.cookie)[name] -} - -export function setClientCookie( - name: string, - value: string, - options?: CookieSerializeOptions, -): void { - if (options?.httpOnly && process.env.NODE_ENV !== 'production') { - console.warn( - '`setCookie` was called with `httpOnly: true` in the browser. Browsers silently discard cookies written via `document.cookie` when `HttpOnly` is set, so this cookie will NOT be set.', - ) - } - document.cookie = serialize(name, value, options) -} +const cookieFns = createCookieFns({ + getCookie: getServerCookie, + setCookie: setServerCookie, +}) /** * Get a cookie value by name. Works on both the server (reads the current @@ -44,9 +18,7 @@ export function setClientCookie( * const authorization = getCookie('Authorization') * ``` */ -export const getCookie: GetCookieFn = createIsomorphicFn() - .server(getServerCookie) - .client(getClientCookie) +export const getCookie = cookieFns.getCookie /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -58,6 +30,4 @@ export const getCookie: GetCookieFn = createIsomorphicFn() * setCookie('Authorization', '1234567') * ``` */ -export const setCookie: SetCookieFn = createIsomorphicFn() - .server(setServerCookie) - .client(setClientCookie) +export const setCookie = cookieFns.setCookie diff --git a/packages/solid-start/tests/cookies.test.ts b/packages/solid-start/tests/cookies.test.ts index 5076f8fa57..2e1dcc2e00 100644 --- a/packages/solid-start/tests/cookies.test.ts +++ b/packages/solid-start/tests/cookies.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' const getServerCookieMock = vi.fn<(name: string) => string | undefined>() const setServerCookieMock = vi.fn() @@ -8,29 +8,18 @@ vi.mock('@tanstack/start-server-core', () => ({ setCookie: setServerCookieMock, })) -const { getCookie, setCookie, getClientCookie, setClientCookie } = - await import('../src/cookies') +const { getCookie, setCookie } = await import('../src/cookies') -function clearDocumentCookies() { - document.cookie.split(';').forEach((cookie) => { - const name = cookie.split('=')[0]?.trim() - if (name) { - document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` - } - }) -} - -describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', () => { +// The cookie logic itself (client-side parsing/serialization, the httpOnly +// warning, and createCookieFns' delegation behavior) is covered by +// start-client-core's own tests. This only verifies that this package wires +// its getCookie/setCookie up to the right start-server-core functions. +describe('getCookie/setCookie', () => { beforeEach(() => { getServerCookieMock.mockReset() setServerCookieMock.mockReset() }) - // Outside of a Start-compiled bundle, createIsomorphicFn's runtime fallback - // always resolves to the `.server()` implementation once one is - // registered (see packages/start-fn-stubs/src/createIsomorphicFn.ts), so - // calling the exported getCookie/setCookie here exercises the server - // delegation, not the client branch. it('delegates getCookie to start-server-core', () => { getServerCookieMock.mockReturnValue('server-value') @@ -48,66 +37,3 @@ describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', ) }) }) - -describe('getClientCookie', () => { - afterEach(() => { - clearDocumentCookies() - }) - - it('reads a cookie value from document.cookie', () => { - document.cookie = 'token=abc123' - - expect(getClientCookie('token')).toBe('abc123') - }) - - it('returns undefined for a cookie that is not present', () => { - expect(getClientCookie('missing')).toBeUndefined() - }) -}) - -describe('setClientCookie', () => { - afterEach(() => { - clearDocumentCookies() - vi.unstubAllEnvs() - vi.restoreAllMocks() - }) - - it('writes the cookie to document.cookie', () => { - setClientCookie('token', 'abc123', { path: '/' }) - - expect(getClientCookie('token')).toBe('abc123') - }) - - it('warns in development when httpOnly is set, since browsers silently discard the cookie', () => { - vi.stubEnv('NODE_ENV', 'development') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123', { httpOnly: true }) - - expect(warnSpy).toHaveBeenCalledExactlyOnceWith( - expect.stringContaining('HttpOnly'), - ) - // Per the WHATWG cookie spec, browsers discard the entire cookie (not - // just the HttpOnly attribute) when it's set via `document.cookie` with - // `HttpOnly` present — this is real behavior, not a quirk of the warning. - expect(getClientCookie('token')).toBeUndefined() - }) - - it('does not warn when httpOnly is not set', () => { - vi.stubEnv('NODE_ENV', 'development') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123') - - expect(warnSpy).not.toHaveBeenCalled() - }) - - it('does not warn in production even when httpOnly is set', () => { - vi.stubEnv('NODE_ENV', 'production') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123', { httpOnly: true }) - - expect(warnSpy).not.toHaveBeenCalled() - }) -}) diff --git a/packages/start-client-core/package.json b/packages/start-client-core/package.json index edbff306fa..5eab7153a2 100644 --- a/packages/start-client-core/package.json +++ b/packages/start-client-core/package.json @@ -60,6 +60,12 @@ "default": "./dist/esm/client-rpc/index.js" } }, + "./cookies": { + "import": { + "types": "./dist/esm/cookies.d.ts", + "default": "./dist/esm/cookies.js" + } + }, "./hydration": { "import": { "types": "./dist/esm/hydration.d.ts", @@ -109,6 +115,7 @@ "@tanstack/router-core": "workspace:*", "@tanstack/start-fn-stubs": "workspace:*", "@tanstack/start-storage-context": "workspace:*", + "cookie-es": "^3.0.0", "seroval": "^1.5.4" }, "devDependencies": { diff --git a/packages/start-client-core/src/cookies.ts b/packages/start-client-core/src/cookies.ts new file mode 100644 index 0000000000..7ef6635e9b --- /dev/null +++ b/packages/start-client-core/src/cookies.ts @@ -0,0 +1,58 @@ +import { createIsomorphicFn } from '@tanstack/start-fn-stubs' +import { parse, serialize } from 'cookie-es' +import type { CookieSerializeOptions } from 'cookie-es' + +export type GetCookieFn = (name: string) => string | undefined +export type SetCookieFn = ( + name: string, + value: string, + options?: CookieSerializeOptions, +) => void + +// Exported (but not part of any package's public entry) so they can be unit +// tested directly, since `createIsomorphicFn`'s uncompiled runtime fallback +// always resolves to the `.server()` implementation once one is registered, +// making the `.client()` branch unreachable through `createCookieFns`'s +// output outside of a Start-compiled bundle. +export function getClientCookie(name: string): string | undefined { + return parse(document.cookie)[name] +} + +export function setClientCookie( + name: string, + value: string, + options?: CookieSerializeOptions, +): void { + if (options?.httpOnly && process.env.NODE_ENV !== 'production') { + console.warn( + '`setCookie` was called with `httpOnly: true` in the browser. Browsers silently discard cookies written via `document.cookie` when `HttpOnly` is set, so this cookie will NOT be set.', + ) + } + document.cookie = serialize(name, value, options) +} + +/** + * Build isomorphic `getCookie`/`setCookie` functions from a server-side + * cookie implementation. On the server, calls delegate to the given + * implementations (e.g. the request-scoped `getCookie`/`setCookie` from + * `@tanstack/start-server-core`); in the browser, they read/write + * `document.cookie` directly. + * + * The server implementation is injected as a parameter rather than imported + * directly: `@tanstack/start-server-core` depends on this package, so a + * static import in the other direction would be a circular package + * dependency. + */ +export function createCookieFns(server: { + getCookie: GetCookieFn + setCookie: SetCookieFn +}): { getCookie: GetCookieFn; setCookie: SetCookieFn } { + return { + getCookie: createIsomorphicFn() + .server(server.getCookie) + .client(getClientCookie), + setCookie: createIsomorphicFn() + .server(server.setCookie) + .client(setClientCookie), + } +} diff --git a/packages/start-client-core/tests/cookies.test.ts b/packages/start-client-core/tests/cookies.test.ts new file mode 100644 index 0000000000..2f63a7129a --- /dev/null +++ b/packages/start-client-core/tests/cookies.test.ts @@ -0,0 +1,119 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + createCookieFns, + getClientCookie, + setClientCookie, +} from '../src/cookies' + +function clearDocumentCookies() { + document.cookie.split(';').forEach((cookie) => { + const name = cookie.split('=')[0]?.trim() + if (name) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + } + }) +} + +describe('createCookieFns (uncompiled createIsomorphicFn runtime fallback)', () => { + const getServerCookieMock = vi.fn<(name: string) => string | undefined>() + const setServerCookieMock = vi.fn() + + beforeEach(() => { + getServerCookieMock.mockReset() + setServerCookieMock.mockReset() + }) + + // Outside of a Start-compiled bundle, createIsomorphicFn's runtime fallback + // always resolves to the `.server()` implementation once one is + // registered (see packages/start-fn-stubs/src/createIsomorphicFn.ts), so + // calling the returned getCookie/setCookie here exercises the server + // delegation, not the client branch. + it('delegates getCookie to the provided server implementation', () => { + getServerCookieMock.mockReturnValue('server-value') + const { getCookie } = createCookieFns({ + getCookie: getServerCookieMock, + setCookie: setServerCookieMock, + }) + + expect(getCookie('token')).toBe('server-value') + expect(getServerCookieMock).toHaveBeenCalledExactlyOnceWith('token') + }) + + it('delegates setCookie to the provided server implementation', () => { + const { setCookie } = createCookieFns({ + getCookie: getServerCookieMock, + setCookie: setServerCookieMock, + }) + + setCookie('token', 'abc123', { path: '/' }) + + expect(setServerCookieMock).toHaveBeenCalledExactlyOnceWith( + 'token', + 'abc123', + { path: '/' }, + ) + }) +}) + +describe('getClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + }) + + it('reads a cookie value from document.cookie', () => { + document.cookie = 'token=abc123' + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('returns undefined for a cookie that is not present', () => { + expect(getClientCookie('missing')).toBeUndefined() + }) +}) + +describe('setClientCookie', () => { + afterEach(() => { + clearDocumentCookies() + vi.unstubAllEnvs() + vi.restoreAllMocks() + }) + + it('writes the cookie to document.cookie', () => { + setClientCookie('token', 'abc123', { path: '/' }) + + expect(getClientCookie('token')).toBe('abc123') + }) + + it('warns in development when httpOnly is set, since browsers silently discard the cookie', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining('HttpOnly'), + ) + // Per the WHATWG cookie spec, browsers discard the entire cookie (not + // just the HttpOnly attribute) when it's set via `document.cookie` with + // `HttpOnly` present. + expect(getClientCookie('token')).toBeUndefined() + }) + + it('does not warn when httpOnly is not set', () => { + vi.stubEnv('NODE_ENV', 'development') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123') + + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('does not warn in production even when httpOnly is set', () => { + vi.stubEnv('NODE_ENV', 'production') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + setClientCookie('token', 'abc123', { httpOnly: true }) + + expect(warnSpy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/start-client-core/vite.config.ts b/packages/start-client-core/vite.config.ts index 71e163a1ed..1ddcd93779 100644 --- a/packages/start-client-core/vite.config.ts +++ b/packages/start-client-core/vite.config.ts @@ -25,6 +25,7 @@ export default defineConfig(({ command }) => './src/index.tsx', './src/client/index.ts', './src/client-rpc/index.ts', + './src/cookies.ts', './src/hydration/constants.ts', './src/hydration.ts', './src/hydration/runtime.ts', diff --git a/packages/vue-start/package.json b/packages/vue-start/package.json index ac816c6b21..3c670fa74c 100644 --- a/packages/vue-start/package.json +++ b/packages/vue-start/package.json @@ -120,7 +120,6 @@ "@tanstack/vue-router": "workspace:*", "@tanstack/vue-start-client": "workspace:*", "@tanstack/vue-start-server": "workspace:*", - "cookie-es": "^3.0.0", "pathe": "^2.0.3" }, "devDependencies": { diff --git a/packages/vue-start/src/cookies.ts b/packages/vue-start/src/cookies.ts index 42e5257a29..037e04b5d6 100644 --- a/packages/vue-start/src/cookies.ts +++ b/packages/vue-start/src/cookies.ts @@ -1,39 +1,13 @@ -import { createIsomorphicFn } from '@tanstack/start-client-core' +import { createCookieFns } from '@tanstack/start-client-core/cookies' import { getCookie as getServerCookie, setCookie as setServerCookie, } from '@tanstack/start-server-core' -import { parse, serialize } from 'cookie-es' -import type { CookieSerializeOptions } from 'cookie-es' -type GetCookieFn = (name: string) => string | undefined -type SetCookieFn = ( - name: string, - value: string, - options?: CookieSerializeOptions, -) => void - -// Exported (but not part of the package's public entry) so they can be unit -// tested directly, since `createIsomorphicFn`'s uncompiled runtime fallback -// always resolves to the `.server()` implementation once one is registered, -// making the `.client()` branch unreachable through `getCookie`/`setCookie` -// outside of a Start-compiled bundle. -export function getClientCookie(name: string): string | undefined { - return parse(document.cookie)[name] -} - -export function setClientCookie( - name: string, - value: string, - options?: CookieSerializeOptions, -): void { - if (options?.httpOnly && process.env.NODE_ENV !== 'production') { - console.warn( - '`setCookie` was called with `httpOnly: true` in the browser. Browsers silently discard cookies written via `document.cookie` when `HttpOnly` is set, so this cookie will NOT be set.', - ) - } - document.cookie = serialize(name, value, options) -} +const cookieFns = createCookieFns({ + getCookie: getServerCookie, + setCookie: setServerCookie, +}) /** * Get a cookie value by name. Works on both the server (reads the current @@ -44,9 +18,7 @@ export function setClientCookie( * const authorization = getCookie('Authorization') * ``` */ -export const getCookie: GetCookieFn = createIsomorphicFn() - .server(getServerCookie) - .client(getClientCookie) +export const getCookie = cookieFns.getCookie /** * Set a cookie value by name. Works on both the server (sets a `Set-Cookie` @@ -58,6 +30,4 @@ export const getCookie: GetCookieFn = createIsomorphicFn() * setCookie('Authorization', '1234567') * ``` */ -export const setCookie: SetCookieFn = createIsomorphicFn() - .server(setServerCookie) - .client(setClientCookie) +export const setCookie = cookieFns.setCookie diff --git a/packages/vue-start/tests/cookies.test.ts b/packages/vue-start/tests/cookies.test.ts index 5076f8fa57..2e1dcc2e00 100644 --- a/packages/vue-start/tests/cookies.test.ts +++ b/packages/vue-start/tests/cookies.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' const getServerCookieMock = vi.fn<(name: string) => string | undefined>() const setServerCookieMock = vi.fn() @@ -8,29 +8,18 @@ vi.mock('@tanstack/start-server-core', () => ({ setCookie: setServerCookieMock, })) -const { getCookie, setCookie, getClientCookie, setClientCookie } = - await import('../src/cookies') +const { getCookie, setCookie } = await import('../src/cookies') -function clearDocumentCookies() { - document.cookie.split(';').forEach((cookie) => { - const name = cookie.split('=')[0]?.trim() - if (name) { - document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` - } - }) -} - -describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', () => { +// The cookie logic itself (client-side parsing/serialization, the httpOnly +// warning, and createCookieFns' delegation behavior) is covered by +// start-client-core's own tests. This only verifies that this package wires +// its getCookie/setCookie up to the right start-server-core functions. +describe('getCookie/setCookie', () => { beforeEach(() => { getServerCookieMock.mockReset() setServerCookieMock.mockReset() }) - // Outside of a Start-compiled bundle, createIsomorphicFn's runtime fallback - // always resolves to the `.server()` implementation once one is - // registered (see packages/start-fn-stubs/src/createIsomorphicFn.ts), so - // calling the exported getCookie/setCookie here exercises the server - // delegation, not the client branch. it('delegates getCookie to start-server-core', () => { getServerCookieMock.mockReturnValue('server-value') @@ -48,66 +37,3 @@ describe('getCookie/setCookie (uncompiled createIsomorphicFn runtime fallback)', ) }) }) - -describe('getClientCookie', () => { - afterEach(() => { - clearDocumentCookies() - }) - - it('reads a cookie value from document.cookie', () => { - document.cookie = 'token=abc123' - - expect(getClientCookie('token')).toBe('abc123') - }) - - it('returns undefined for a cookie that is not present', () => { - expect(getClientCookie('missing')).toBeUndefined() - }) -}) - -describe('setClientCookie', () => { - afterEach(() => { - clearDocumentCookies() - vi.unstubAllEnvs() - vi.restoreAllMocks() - }) - - it('writes the cookie to document.cookie', () => { - setClientCookie('token', 'abc123', { path: '/' }) - - expect(getClientCookie('token')).toBe('abc123') - }) - - it('warns in development when httpOnly is set, since browsers silently discard the cookie', () => { - vi.stubEnv('NODE_ENV', 'development') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123', { httpOnly: true }) - - expect(warnSpy).toHaveBeenCalledExactlyOnceWith( - expect.stringContaining('HttpOnly'), - ) - // Per the WHATWG cookie spec, browsers discard the entire cookie (not - // just the HttpOnly attribute) when it's set via `document.cookie` with - // `HttpOnly` present — this is real behavior, not a quirk of the warning. - expect(getClientCookie('token')).toBeUndefined() - }) - - it('does not warn when httpOnly is not set', () => { - vi.stubEnv('NODE_ENV', 'development') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123') - - expect(warnSpy).not.toHaveBeenCalled() - }) - - it('does not warn in production even when httpOnly is set', () => { - vi.stubEnv('NODE_ENV', 'production') - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - setClientCookie('token', 'abc123', { httpOnly: true }) - - expect(warnSpy).not.toHaveBeenCalled() - }) -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c35eb0b35..efbb5044d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12708,9 +12708,6 @@ importers: '@tanstack/start-server-core': specifier: workspace:* version: link:../start-server-core - cookie-es: - specifier: ^3.0.0 - version: 3.1.1 pathe: specifier: ^2.0.3 version: 2.0.3 @@ -13209,9 +13206,6 @@ importers: '@tanstack/start-server-core': specifier: workspace:* version: link:../start-server-core - cookie-es: - specifier: ^3.0.0 - version: 3.1.1 pathe: specifier: ^2.0.3 version: 2.0.3 @@ -13293,6 +13287,9 @@ importers: '@tanstack/start-storage-context': specifier: workspace:* version: link:../start-storage-context + cookie-es: + specifier: ^3.0.0 + version: 3.1.1 seroval: specifier: ^1.5.4 version: 1.5.4 @@ -13607,9 +13604,6 @@ importers: '@tanstack/vue-start-server': specifier: workspace:* version: link:../vue-start-server - cookie-es: - specifier: ^3.0.0 - version: 3.1.1 pathe: specifier: ^2.0.3 version: 2.0.3