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) diff --git a/packages/react-start/package.json b/packages/react-start/package.json index b037e44f8f..a2e192c155 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 new file mode 100644 index 0000000000..037e04b5d6 --- /dev/null +++ b/packages/react-start/src/cookies.ts @@ -0,0 +1,33 @@ +import { createCookieFns } from '@tanstack/start-client-core/cookies' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' + +const cookieFns = createCookieFns({ + getCookie: getServerCookie, + setCookie: setServerCookie, +}) + +/** + * 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 = cookieFns.getCookie + +/** + * 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 = cookieFns.setCookie 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` diff --git a/packages/react-start/tests/cookies.test.ts b/packages/react-start/tests/cookies.test.ts new file mode 100644 index 0000000000..2e1dcc2e00 --- /dev/null +++ b/packages/react-start/tests/cookies.test.ts @@ -0,0 +1,39 @@ +import { 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 } = await import('../src/cookies') + +// 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() + }) + + 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: '/' }, + ) + }) +}) diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index 4b11e9a219..20d0b576a9 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 new file mode 100644 index 0000000000..037e04b5d6 --- /dev/null +++ b/packages/solid-start/src/cookies.ts @@ -0,0 +1,33 @@ +import { createCookieFns } from '@tanstack/start-client-core/cookies' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' + +const cookieFns = createCookieFns({ + getCookie: getServerCookie, + setCookie: setServerCookie, +}) + +/** + * 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 = cookieFns.getCookie + +/** + * 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 = cookieFns.setCookie 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` diff --git a/packages/solid-start/tests/cookies.test.ts b/packages/solid-start/tests/cookies.test.ts new file mode 100644 index 0000000000..2e1dcc2e00 --- /dev/null +++ b/packages/solid-start/tests/cookies.test.ts @@ -0,0 +1,39 @@ +import { 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 } = await import('../src/cookies') + +// 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() + }) + + 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: '/' }, + ) + }) +}) 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 5afd0d06a5..3c670fa74c 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 new file mode 100644 index 0000000000..037e04b5d6 --- /dev/null +++ b/packages/vue-start/src/cookies.ts @@ -0,0 +1,33 @@ +import { createCookieFns } from '@tanstack/start-client-core/cookies' +import { + getCookie as getServerCookie, + setCookie as setServerCookie, +} from '@tanstack/start-server-core' + +const cookieFns = createCookieFns({ + getCookie: getServerCookie, + setCookie: setServerCookie, +}) + +/** + * 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 = cookieFns.getCookie + +/** + * 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 = cookieFns.setCookie 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` diff --git a/packages/vue-start/tests/cookies.test.ts b/packages/vue-start/tests/cookies.test.ts new file mode 100644 index 0000000000..2e1dcc2e00 --- /dev/null +++ b/packages/vue-start/tests/cookies.test.ts @@ -0,0 +1,39 @@ +import { 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 } = await import('../src/cookies') + +// 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() + }) + + 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: '/' }, + ) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a7c3a7348..efbb5044d4 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 @@ -13287,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 @@ -21793,21 +21796,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 +22574,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 +22663,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 +22771,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 +23856,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 +24083,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 +24886,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 +25399,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 +25815,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 +25978,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 +26096,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 +26170,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 +26191,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 +26244,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 +27676,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 +27684,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 +27995,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 +28006,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 +30212,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 +33527,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 +34026,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 +34039,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 +34573,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 +34581,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 +35342,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 +35807,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 +36920,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 +36990,6 @@ snapshots: hookable@6.1.0: {} - hookable@6.1.1: {} - hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -37166,8 +37140,6 @@ snapshots: httpxy@0.3.1: {} - httpxy@0.5.3: {} - human-id@4.1.1: {} human-signals@5.0.0: {} @@ -37281,7 +37253,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 +38213,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 +38253,6 @@ snapshots: - '@electric-sql/pglite' - '@libsql/client' - '@netlify/blobs' - - '@netlify/runtime' - '@planetscale/database' - '@upstash/redis' - '@vercel/blob' @@ -38293,7 +38265,6 @@ snapshots: - idb-keyval - ioredis - lru-cache - - miniflare - mongodb - mysql2 - sqlite3 @@ -38585,7 +38556,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 +38571,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 +38722,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 +39664,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 +40301,8 @@ snapshots: srvx@0.11.15: {} + srvx@0.8.16: {} + stable-hash-x@0.2.0: {} stack-trace@0.0.10: {} @@ -40850,6 +40824,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 +40933,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