From 1527eb1ab87f97778b9b9171fa62fbbfc7a64e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Mon, 21 Oct 2024 19:16:55 -0300 Subject: [PATCH 01/30] refactor: update to async cookie handling and bump major version to 5.0.0 --- package.json | 4 ++-- src/index.ts | 54 ++++++++++++++++++++++++++++++++++------------------ src/types.ts | 5 +++-- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index b139fce..7badd8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cookies-next", - "version": "4.3.0", + "version": "5.0.0", "description": "Getting, setting and removing cookies on both client and server with next.js", "main": "lib/index.js", "type": "lib/index.d.ts", @@ -40,7 +40,7 @@ }, "devDependencies": { "@types/node": "^16.10.2", - "next": "^13.4.19", + "next": "^15.0.0", "prettier": "^3.0.2", "typescript": "^4.4.3" } diff --git a/src/index.ts b/src/index.ts index 1f3ca08..59d585a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,16 +24,34 @@ const isCookiesFromAppRouter = ( ); }; -const isContextFromAppRouter = ( - context?: OptionsType, +const isPotentialContextFromAppRouter = ( + context?: OptionsType ): context is { res?: NextResponse; req?: NextRequest; cookies?: CookiesFn } => { return ( - (!!context?.req && 'cookies' in context.req && isCookiesFromAppRouter(context?.req.cookies)) || - (!!context?.res && 'cookies' in context.res && isCookiesFromAppRouter(context?.res.cookies)) || - (!!context?.cookies && isCookiesFromAppRouter(context.cookies())) + (!!context?.req && 'cookies' in context.req) || + (!!context?.res && 'cookies' in context.res) || + (!!context?.cookies && typeof context.cookies === 'function') ); }; +const validateContextCookies = async (context: { + res?: NextResponse; + req?: NextRequest; + cookies?: CookiesFn; +}): Promise => { + if (context.req && 'cookies' in context.req) { + return isCookiesFromAppRouter(context.req.cookies); + } + if (context.res && 'cookies' in context.res) { + return isCookiesFromAppRouter(context.res.cookies); + } + if (context.cookies) { + const cookies = await context.cookies(); + return isCookiesFromAppRouter(cookies); + } + return false; +}; + const transformAppRouterCookies = (cookies: AppRouterCookies): TmpCookiesObj => { let _cookies: Partial = {}; @@ -61,18 +79,18 @@ const decode = (str: string): string => { return str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); }; -export const getCookies = (options?: OptionsType): TmpCookiesObj => { - if (isContextFromAppRouter(options)) { +export const getCookies = async (options?: OptionsType): Promise => { + if (isPotentialContextFromAppRouter(options)) { if (options?.req) { return transformAppRouterCookies(options.req.cookies); } if (options?.cookies) { - return transformAppRouterCookies(options.cookies()); + return transformAppRouterCookies(await options.cookies()); } } let req; - // DefaultOptions['req] can be casted here because is narrowed by using the fn: isContextFromAppRouter + // DefaultOptions['req] can be casted here because is narrowed by using the fn: isPotentialContextFromAppRouter if (options) req = options.req as DefaultOptions['req']; if (!isClientSide()) { @@ -99,17 +117,18 @@ export const getCookies = (options?: OptionsType): TmpCookiesObj => { return _cookies; }; -export const getCookie = (key: string, options?: OptionsType): CookieValueTypes => { - const _cookies = getCookies(options); +export const getCookie = async (key: string, options?: OptionsType): Promise => { + const _cookies = await getCookies(options); const value = _cookies[key]; if (value === undefined) return undefined; return decode(value); }; -export const setCookie = (key: string, data: any, options?: OptionsType): void => { - if (isContextFromAppRouter(options)) { +export const setCookie = async (key: string, data: any, options?: OptionsType): Promise => { + if (isPotentialContextFromAppRouter(options)) { const { req, res, cookies: cookiesFn, ...restOptions } = options; const payload = { name: key, value: stringify(data), ...restOptions }; + if (req) { req.cookies.set(payload); } @@ -117,7 +136,7 @@ export const setCookie = (key: string, data: any, options?: OptionsType): void = res.cookies.set(payload); } if (cookiesFn) { - cookiesFn().set(payload); + (await cookiesFn()).set(payload); } return; } @@ -162,13 +181,12 @@ export const setCookie = (key: string, data: any, options?: OptionsType): void = } }; -export const deleteCookie = (key: string, options?: OptionsType): void => { +export const deleteCookie = async (key: string, options?: OptionsType): Promise => { return setCookie(key, '', { ...options, maxAge: -1 }); }; -export const hasCookie = (key: string, options?: OptionsType): boolean => { +export const hasCookie = async (key: string, options?: OptionsType): Promise => { if (!key) return false; - - const cookie = getCookies(options); + const cookie = await getCookies(options); return cookie.hasOwnProperty(key); }; diff --git a/src/types.ts b/src/types.ts index 2706f28..93ea82f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import { CookieSerializeOptions } from 'cookie'; import { IncomingMessage, ServerResponse } from 'http'; import type { NextRequest, NextResponse } from 'next/server'; import type { cookies } from 'next/headers'; +import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'; export type OptionsType = DefaultOptions | AppRouterOptions; export interface DefaultOptions extends CookieSerializeOptions { @@ -12,7 +13,7 @@ export interface DefaultOptions extends CookieSerializeOptions { cookies?: CookiesFn; } -export type CookiesFn = typeof cookies; +export type CookiesFn = () => Promise>; export type AppRouterOptions = { res?: Response | NextResponse; req?: Request | NextRequest; @@ -20,4 +21,4 @@ export type AppRouterOptions = { }; export type AppRouterCookies = NextResponse['cookies'] | NextRequest['cookies']; export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>; -export type CookieValueTypes = string | undefined; +export type CookieValueTypes = string | undefined; \ No newline at end of file From f89b35e881799be1eb0a6b4816884fee18cc1c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Mon, 21 Oct 2024 19:33:11 -0300 Subject: [PATCH 02/30] feat: enhance cookie handling and add jest testing support --- README.md | 18 +++--- jest.config.js | 4 ++ package.json | 11 +++- src/cookies.test.ts | 145 ++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 12 ++-- src/types.ts | 3 +- 6 files changed, 175 insertions(+), 18 deletions(-) create mode 100644 jest.config.js create mode 100644 src/cookies.test.ts diff --git a/README.md b/README.md index 12e4e9f..d58f49a 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,10 @@ as the first argument to the function and when server side rendering, this funct In Next.js 13+, you can [read(only)](https://nextjs.org/docs/app/api-reference/functions/cookies) cookies in `Server Components` and read/update them in `Server Actions`. This can be achieved by using the `cookies` function as an option, which is imported from `next/headers`, instead of using `req` and `res`. -#### Client - App Router Example +#### Client - App Router Example ```ts -'use client' +'use client'; import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next'; getCookies(); @@ -97,6 +97,7 @@ getCookie('key'); setCookie('key', 'value'); deleteCookie('key'); ``` + #### Client - Pages Router Example ```js @@ -121,7 +122,7 @@ const Home = async () => { // It's not possible to update the cookie in RSC ❌ setCookie("test", "value", { cookies }); 👉🏻// Won't work. ❌ deleteCookie('test1', { cookies }); 👉🏻// Won't work. - + ✔️ getCookie('test1', { cookies }); ✔️ getCookies({ cookies }); ✔️ hasCookie('test1', { cookies }); @@ -136,6 +137,7 @@ const Home = async () => { export default Home; ``` + #### SSR - Pages Router Example `/page/index.js` @@ -159,7 +161,9 @@ export const getServerSideProps = ({ req, res }) => { export default Home; ``` + #### SSR - Server Actions Example + ```ts 'use server'; @@ -173,9 +177,6 @@ export async function testAction() { hasCookie('test', { cookies }); deleteCookie('test', { cookies }); } - - - ``` #### API - Pages Router Example @@ -195,6 +196,7 @@ export default async function handler(req, res) { return res.status(200).json({ message: 'ok' }); } ``` + #### API - App Router Example `/app/api/hello/route.ts` @@ -221,9 +223,9 @@ export async function GET(req: NextRequest) { return res; } - ``` -#### Middleware + +#### Middleware ```ts import { NextResponse } from 'next/server'; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..75f4a49 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jest-environment-jsdom', +}; diff --git a/package.json b/package.json index 7badd8a..ed77e4b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "lib/index.d.ts", "scripts": { "build": "tsc", - "pretty": "npx prettier . --write" + "pretty": "npx prettier . --write", + "test": "jest" }, "repository": { "type": "git", @@ -35,13 +36,17 @@ }, "homepage": "https://github.com/andreizanik/cookies-next#readme", "dependencies": { - "cookie": "^0.7.0", - "@types/cookie": "^0.6.0" + "@types/cookie": "^0.6.0", + "cookie": "^0.7.0" }, "devDependencies": { + "@types/jest": "^29.5.13", "@types/node": "^16.10.2", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "next": "^15.0.0", "prettier": "^3.0.2", + "ts-jest": "^29.2.5", "typescript": "^4.4.3" } } diff --git a/src/cookies.test.ts b/src/cookies.test.ts new file mode 100644 index 0000000..8ea04fe --- /dev/null +++ b/src/cookies.test.ts @@ -0,0 +1,145 @@ +import { getCookies, getCookie, setCookie, deleteCookie, hasCookie, validateContextCookies } from './index'; +import type { OptionsType, AppRouterCookies } from './types'; +import { NextRequest, NextResponse } from 'next/server'; + +// Mock implementations +const mockAppRouterCookies: AppRouterCookies = { + getAll: jest.fn().mockReturnValue([ + { name: 'test', value: 'value' }, + { name: 'jsonCookie', value: JSON.stringify({ foo: 'bar' }) } + ]), + set: jest.fn(), + get: jest.fn(), + has: jest.fn(), + delete: jest.fn(), + clear: jest.fn(), + [Symbol.iterator]: jest.fn(), + size: 2, +}; + +const mockOptions = { + req: { + cookies: mockAppRouterCookies, + } as unknown as NextRequest, + res: { + cookies: mockAppRouterCookies, + } as unknown as NextResponse, + cookies: jest.fn().mockResolvedValue(mockAppRouterCookies), +} satisfies OptionsType; + +describe('Cookie Functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Reset document.cookie before each test + Object.defineProperty(document, 'cookie', { + writable: true, + value: '', + }); + }); + + describe('validateContextCookies', () => { + test('should return true for valid AppRouterCookies', async () => { + const result = await validateContextCookies(mockOptions); + expect(result).toBe(true); + }); + + test('should return false for invalid context', async () => { + const result = await validateContextCookies({}); + expect(result).toBe(false); + }); + }); + + describe('getCookies', () => { + test('should return transformed cookies for valid context', async () => { + const cookies = await getCookies(mockOptions); + expect(cookies).toEqual({ test: 'value', jsonCookie: JSON.stringify({ foo: 'bar' }) }); + }); + + test('should return empty object for client-side without cookies', async () => { + const cookies = await getCookies(); + expect(cookies).toEqual({}); + }); + + test('should parse client-side cookies', async () => { + document.cookie = 'key1=value1; key2=value2'; + const cookies = await getCookies(); + expect(cookies).toEqual({ key1: 'value1', key2: 'value2' }); + }); + }); + + describe('getCookie', () => { + test('should return specific cookie value for valid context', async () => { + const value = await getCookie('test', mockOptions); + expect(value).toBe('value'); + }); + + test('should return undefined for non-existent cookie', async () => { + const value = await getCookie('nonexistent', mockOptions); + expect(value).toBeUndefined(); + }); + + test('should parse JSON cookie value', async () => { + const value = await getCookie('jsonCookie', mockOptions); + expect(value).toBe('{"foo":"bar"}'); + }); + }); + + describe('setCookie', () => { + test('should set a cookie in AppRouterCookies', async () => { + await setCookie('newKey', 'newValue', mockOptions); + expect(mockAppRouterCookies.set).toHaveBeenCalledWith({ + name: 'newKey', + value: 'newValue', + }); + }); + + test('should set a cookie on client-side', async () => { + await setCookie('clientKey', 'clientValue'); + expect(document.cookie).toContain('clientKey=clientValue'); + }); + + test('should stringify non-string values', async () => { + await setCookie('objectKey', { foo: 'bar' }, mockOptions); + expect(mockAppRouterCookies.set).toHaveBeenCalledWith({ + name: 'objectKey', + value: '{"foo":"bar"}', + }); + }); + }); + + describe('deleteCookie', () => { + test('should delete a cookie in AppRouterCookies', async () => { + await deleteCookie('test', mockOptions); + expect(mockAppRouterCookies.set).toHaveBeenCalledWith({ + name: 'test', + value: '', + maxAge: -1, + }); + }); + + test('should delete a cookie on client-side', async () => { + document.cookie = 'toDelete=value; path=/'; + await deleteCookie('toDelete'); + expect(document.cookie).not.toContain('toDelete=value'); + }); + }); + + describe('hasCookie', () => { + test('should return true for existing cookie in AppRouterCookies', async () => { + const result = await hasCookie('test', mockOptions); + expect(result).toBe(true); + }); + + test('should return false for non-existent cookie', async () => { + const result = await hasCookie('nonexistent', mockOptions); + expect(result).toBe(false); + }); + + test('should check for cookie existence on client-side', async () => { + document.cookie = 'existingCookie=value; path=/'; + const result = await hasCookie('existingCookie'); + expect(result).toBe(true); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index 59d585a..a1eb8df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ const isCookiesFromAppRouter = ( }; const isPotentialContextFromAppRouter = ( - context?: OptionsType + context?: OptionsType, ): context is { res?: NextResponse; req?: NextRequest; cookies?: CookiesFn } => { return ( (!!context?.req && 'cookies' in context.req) || @@ -34,7 +34,7 @@ const isPotentialContextFromAppRouter = ( ); }; -const validateContextCookies = async (context: { +export const validateContextCookies = async (context: { res?: NextResponse; req?: NextRequest; cookies?: CookiesFn; @@ -80,12 +80,14 @@ const decode = (str: string): string => { }; export const getCookies = async (options?: OptionsType): Promise => { - if (isPotentialContextFromAppRouter(options)) { + if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { if (options?.req) { return transformAppRouterCookies(options.req.cookies); } if (options?.cookies) { - return transformAppRouterCookies(await options.cookies()); + if (await validateContextCookies({ cookies: options.cookies })) { + return transformAppRouterCookies(await options.cookies()); + } } } @@ -125,7 +127,7 @@ export const getCookie = async (key: string, options?: OptionsType): Promise => { - if (isPotentialContextFromAppRouter(options)) { + if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { const { req, res, cookies: cookiesFn, ...restOptions } = options; const payload = { name: key, value: stringify(data), ...restOptions }; diff --git a/src/types.ts b/src/types.ts index 93ea82f..223d0bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,6 @@ import { CookieSerializeOptions } from 'cookie'; import { IncomingMessage, ServerResponse } from 'http'; import type { NextRequest, NextResponse } from 'next/server'; import type { cookies } from 'next/headers'; -import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'; export type OptionsType = DefaultOptions | AppRouterOptions; export interface DefaultOptions extends CookieSerializeOptions { @@ -21,4 +20,4 @@ export type AppRouterOptions = { }; export type AppRouterCookies = NextResponse['cookies'] | NextRequest['cookies']; export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>; -export type CookieValueTypes = string | undefined; \ No newline at end of file +export type CookieValueTypes = string | undefined; From 6d41c0d158c87236801552cf3171a66ca80da25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Mon, 21 Oct 2024 21:04:31 -0300 Subject: [PATCH 03/30] refactor(types): remove promise from CookiesFn return type --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 223d0bb..e30c714 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,7 @@ export interface DefaultOptions extends CookieSerializeOptions { cookies?: CookiesFn; } -export type CookiesFn = () => Promise>; +export type CookiesFn = () => ReturnType; export type AppRouterOptions = { res?: Response | NextResponse; req?: Request | NextRequest; From 754bbc1c76d8b7429f044b47d0bd364bff788ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Tue, 22 Oct 2024 08:27:55 -0300 Subject: [PATCH 04/30] docs: update readme for next.js cookie management library usage and features --- README.md | 352 +++++++++++++++--------------------------------------- 1 file changed, 94 insertions(+), 258 deletions(-) diff --git a/README.md b/README.md index d58f49a..96b889c 100644 --- a/README.md +++ b/README.md @@ -3,192 +3,142 @@ [![npm version](https://badge.fury.io/js/cookies-next.svg)](https://badge.fury.io/js/cookies-next) ![GitHub code size in bytes](https://img.shields.io/bundlephobia/min/cookies-next?style=plastic) -Getting, setting and removing cookies with NEXT.JS +A versatile cookie management library for Next.js applications, supporting both client-side and server-side operations. -- can be used on the client side, anywhere -- can be used for server side rendering in getServerSideProps -- can be used in API handlers -- can be used in Next.js 13+ +## Features + +- Works on client-side, server-side rendering, and API routes +- Supports Next.js 13+ App Router and Server Components +- TypeScript compatible +- Lightweight and easy to use ## Installation -``` +```bash npm install --save cookies-next ``` -If you are using next.js version greater than `12.2.0` you need to use cookies-next version `2.1.0` or later - -## Usage - -Create a cookie: +For Next.js versions 15 and above, use the latest version of cookies-next. -```js -import { setCookie } from 'cookies-next'; +For Next.js versions 12.2.0 to 13.x, use cookies-next version 4.3.0: -setCookie('key', 'value', options); +```bash +npm install --save cookies-next@5.0.0 ``` -Read a cookie: +## Usage -```js -import { getCookie } from 'cookies-next'; +### Importing -getCookie('key', options); // => 'value' -getCookie('nothing', options); // => undefined +```javascript +import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from 'cookies-next'; ``` -Read all cookies: +### Basic Operations -```js -import { getCookies } from 'cookies-next'; +#### Set a cookie -getCookies(options); // => { 'name1': 'value1', name2: 'value2' } +```javascript +setCookie('key', 'value', options); ``` -Check if a cookie exists: - -```js -import { hasCookie } from 'cookies-next'; +#### Get a cookie -hasCookie('name', options); // => true -hasCookie('nothing', options); // => false +```javascript +const value = getCookie('key', options); ``` -Delete a cookie: +#### Get all cookies -```js -import { deleteCookie } from 'cookies-next'; - -deleteCookie(name, options); +```javascript +const cookies = getCookies(options); ``` -_IMPORTANT! When deleting a cookie and you're not relying on the default attributes, -you must pass the exact same path and domain attributes that were used to set the cookie:_ - -```js -import { deleteCookie } from 'cookies-next'; +#### Check if a cookie exists -deleteCookie(name, { path: '/path', domain: '.yourdomain.com' }); +```javascript +const exists = hasCookie('key', options); ``` -### Performance - -The [time complexity](https://en.wikipedia.org/wiki/Time_complexity) of all operations is linear with the number of cookies. -For example, under the hood, `getCookie` calls `getCookies`. When working reading multiple cookies, -it is fastest to use `getCookies` and inspect the returned object. +#### Delete a cookie -## Client and Server - -If you pass ctx (Next.js context) in function, then this function will be done on both client and server - -If the function should be done only on client or can't get ctx, pass null or {} -as the first argument to the function and when server side rendering, this function return undefined; - -In Next.js 13+, you can [read(only)](https://nextjs.org/docs/app/api-reference/functions/cookies) cookies in `Server Components` and read/update them in `Server Actions`. This can be achieved by using the `cookies` function as an option, which is imported from `next/headers`, instead of using `req` and `res`. - -#### Client - App Router Example - -```ts -'use client'; -import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next'; - -getCookies(); -getCookie('key'); -setCookie('key', 'value'); -deleteCookie('key'); +```javascript +deleteCookie('key', options); ``` -#### Client - Pages Router Example +### Client-side Usage -```js +```javascript +'use client'; import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next'; -// we can use it anywhere +// Use anywhere in client-side code getCookies(); getCookie('key'); setCookie('key', 'value'); deleteCookie('key'); ``` -#### SSR - App Router Example - -`/app/page.tsx` - -```tsx -import { setCookie, getCookie, getCookies, deleteCookie, hasCookie } from 'cookies-next'; -import { cookies } from 'next/headers'; - -const Home = async () => { - // It's not possible to update the cookie in RSC - ❌ setCookie("test", "value", { cookies }); 👉🏻// Won't work. - ❌ deleteCookie('test1', { cookies }); 👉🏻// Won't work. - - ✔️ getCookie('test1', { cookies }); - ✔️ getCookies({ cookies }); - ✔️ hasCookie('test1', { cookies }); - - return ( -
-

Hello cookies next

-
- ); -}; - -export default Home; - -``` - -#### SSR - Pages Router Example +### Server-side Usage (Pages Router) -`/page/index.js` +In `getServerSideProps`: -```jsx -import React from 'react'; +```javascript import { getCookies, getCookie, setCookie, deleteCookie } from 'cookies-next'; -const Home = () => { - return
page content
; -}; - export const getServerSideProps = ({ req, res }) => { - setCookie('test', 'value', { req, res, maxAge: 60 * 6 * 24 }); + setCookie('test', 'value', { req, res, maxAge: 60 * 60 * 24 }); getCookie('test', { req, res }); getCookies({ req, res }); deleteCookie('test', { req, res }); return { props: {} }; }; +``` -export default Home; +### Server-side Usage (App Router) + +In Server Components: + +```javascript +import { getCookie, getCookies, hasCookie } from 'cookies-next'; +import { cookies } from 'next/headers'; + +const ServerComponent = async () => { + // Read-only operations in Server Components + const value = getCookie('test', { cookies }); + const allCookies = getCookies({ cookies }); + const exists = hasCookie('test', { cookies }); + + // Note: setCookie and deleteCookie cannot be used in Server Components + return
...
; +}; ``` -#### SSR - Server Actions Example +In Server Actions: -```ts +```javascript 'use server'; import { cookies } from 'next/headers'; -import { setCookie, deleteCookie, hasCookie, getCookie, getCookies } from 'cookies-next'; +import { setCookie, deleteCookie, getCookie, getCookies, hasCookie } from 'cookies-next'; -export async function testAction() { +export async function serverAction() { setCookie('test', 'value', { cookies }); + deleteCookie('test', { cookies }); getCookie('test', { cookies }); getCookies({ cookies }); hasCookie('test', { cookies }); - deleteCookie('test', { cookies }); } ``` -#### API - Pages Router Example +### API Routes (Pages Router) -`/page/api/example.js` - -```js -import type { NextApiRequest, NextApiResponse } from 'next'; +```javascript import { getCookies, getCookie, setCookie, deleteCookie } from 'cookies-next'; export default async function handler(req, res) { - setCookie('server-key', 'value', { req, res, maxAge: 60 * 60 * 24 }); + setCookie('key', 'value', { req, res, maxAge: 60 * 60 * 24 }); getCookie('key', { req, res }); getCookies({ req, res }); deleteCookie('key', { req, res }); @@ -197,13 +147,11 @@ export default async function handler(req, res) { } ``` -#### API - App Router Example - -`/app/api/hello/route.ts` +### API Routes (App Router) -```ts +```javascript import { cookies } from 'next/headers'; -import type { NextRequest, NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; import { deleteCookie, getCookie, setCookie, hasCookie, getCookies } from 'cookies-next'; export async function GET(req: NextRequest) { @@ -214,7 +162,7 @@ export async function GET(req: NextRequest) { deleteCookie('test', { res, req }); hasCookie('test', { req, res }); - // provide cookies fn + // Using cookies function setCookie('test1', 'value', { cookies }); getCookie('test1', { cookies }); getCookies({ cookies }); @@ -225,13 +173,12 @@ export async function GET(req: NextRequest) { } ``` -#### Middleware +### Middleware -```ts +```javascript import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { getCookie, setCookie, deleteCookie, hasCookie, getCookies } from 'cookies-next'; -import { cookies } from 'next/headers'; export function middleware(req: NextRequest) { const res = NextResponse.next(); @@ -241,157 +188,46 @@ export function middleware(req: NextRequest) { getCookie('test', { res, req }); getCookies({ res, req }); - ❌ setCookie('test', 'value', { cookies }); 👉🏻// Won't work. - // It's not possible to use cookies function from next/headers in middleware. + // Note: cookies function from next/headers cannot be used in middleware return res; } - ``` ## API -## setCookie(key, value, options); - -```js -setCookie('key', 'value', options); - -setCookie('key', 'value'); // - client side -setCookie('key', 'value', { req, res }); // - server side -setCookie({ cookies }); // - server side(route handlers, server actions) -``` - -## getCookies(options); - -```js -getCookies(); // - client side -getCookies({ req, res }); // - server side -getCookies({ cookies }); // - server side(route handlers, server actions, server components, middleware) -``` - -## getCookie(key, options); - -```js -getCookie('key'); // - client side -getCookie('key', { req, res }); // - server side -getCookie('key', { cookies }); // - server side(route handlers, server actions, server components, middleware) -``` - -## hasCookie(key, options); - -```js -hasCookie('key'); // - client side -hasCookie('key', { req, res }); // - server side -hasCookie('key', { cookies }); // server side(route handlers, server actions, server components, middleware) -``` - -### deleteCookie(key, options); - -```js -deleteCookie('key'); // - client side -deleteCookie('key', { req, res }); // - server side -deleteCookie('key', { cookies }); // - server side(route handlers, server actions) -``` - -_IMPORTANT! When deleting a cookie and you're not relying on the default attributes, -you must pass the exact same path and domain attributes that were used to set the cookie:_ - -```js -deleteCookie(ctx, name, { path: '/path', domain: '.yourdomain.com' }); - client side -deleteCookie(ctx, name, { req, res, path: '/path', domain: '.yourdomain.com' }); - server side -``` - -#### key - -cookie's name - -#### value - -cookie's value - -#### options: - -##### req - -required for server side cookies (route handlers, middleware, API and getServerSideProps) - -##### res - -required for server side cookies (route handlers, middleware, API and getServerSideProps) - -##### cookies - -required for server actions and can be used in route handlers - -##### domain - -Specifies the value for the [`Domain` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3). By default, no -domain is set, and most clients will consider the cookie to apply to only the current domain. - -##### encode - -Specifies a function that will be used to encode a cookie's value. Since value of a cookie -has a limited character set (and must be a simple string), this function can be used to encode -a value into a string suited for a cookie's value. - -The default function is the global `encodeURIComponent`, which will encode a JavaScript string -into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range. - -##### expires - -A `Date` object indicating the cookie's expiration date - -By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and -will delete it on a condition like exiting a web browser application. - -**note** the [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and -`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, -so if both are set, they should point to the same date and time. - -##### httpOnly - -Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy, -the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set. - -**note** be careful when setting this to `true`, as compliant clients will not allow client-side -JavaScript to see the cookie in `document.cookie`. - -##### maxAge +### setCookie(key, value, options) -Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.2). -The given number will be converted to an integer by rounding down. By default, no maximum age is set. +Sets a cookie. -**note** the [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and -`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, -so if both are set, they should point to the same date and time. +### getCookie(key, options) -##### path +Retrieves a specific cookie. -Specifies the value for the [`Path` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4). By default, the path -is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4). +### getCookies(options) -##### sameSite +Retrieves all cookies. -Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7). +### hasCookie(key, options) -- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. -- `false` will not set the `SameSite` attribute. -- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. -- `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. -- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. +Checks if a cookie exists. -More information about the different enforcement levels can be found in -[the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7). +### deleteCookie(key, options) -**note** This is an attribute that has not yet been fully standardized, and may change in the future. -This also means many clients may ignore this attribute until they understand it. +Deletes a cookie. -##### secure +## Options -Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy, -the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. +- `req`: Required for server-side operations (except when using `cookies` function) +- `res`: Required for server-side operations (except when using `cookies` function) +- `cookies`: Function from `next/headers`, used in App Router for server-side operations +- `domain`: Specifies the cookie's domain +- `path`: Specifies the cookie's path +- `maxAge`: Specifies the cookie's maximum age in seconds +- `httpOnly`: Sets the HttpOnly flag +- `secure`: Sets the Secure flag +- `sameSite`: Sets the SameSite attribute ('strict', 'lax', or 'none') -**note** be careful when setting this to `true`, as compliant clients will not send the cookie back to -the server in the future if the browser does not have an HTTPS connection. +For more detailed options, refer to the [cookie specification](https://tools.ietf.org/html/rfc6265). ## License From 1aebc01fdfa39d21cc49b6599dc272a6be85af2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Tue, 22 Oct 2024 08:46:38 -0300 Subject: [PATCH 05/30] docs: update readme to reflect async usage of cookie functions --- README.md | 114 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 96b889c..e006e3a 100644 --- a/README.md +++ b/README.md @@ -39,31 +39,31 @@ import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from 'cooki #### Set a cookie ```javascript -setCookie('key', 'value', options); +await setCookie('key', 'value', options); ``` #### Get a cookie ```javascript -const value = getCookie('key', options); +const value = await getCookie('key', options); ``` #### Get all cookies ```javascript -const cookies = getCookies(options); +const cookies = await getCookies(options); ``` #### Check if a cookie exists ```javascript -const exists = hasCookie('key', options); +const exists = await hasCookie('key', options); ``` #### Delete a cookie ```javascript -deleteCookie('key', options); +await deleteCookie('key', options); ``` ### Client-side Usage @@ -73,10 +73,10 @@ deleteCookie('key', options); import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next'; // Use anywhere in client-side code -getCookies(); -getCookie('key'); -setCookie('key', 'value'); -deleteCookie('key'); +await getCookies(); +await getCookie('key'); +await setCookie('key', 'value'); +await deleteCookie('key'); ``` ### Server-side Usage (Pages Router) @@ -86,11 +86,11 @@ In `getServerSideProps`: ```javascript import { getCookies, getCookie, setCookie, deleteCookie } from 'cookies-next'; -export const getServerSideProps = ({ req, res }) => { - setCookie('test', 'value', { req, res, maxAge: 60 * 60 * 24 }); - getCookie('test', { req, res }); - getCookies({ req, res }); - deleteCookie('test', { req, res }); +export const getServerSideProps = async ({ req, res }) => { + await setCookie('test', 'value', { req, res, maxAge: 60 * 60 * 24 }); + await getCookie('test', { req, res }); + await getCookies({ req, res }); + await deleteCookie('test', { req, res }); return { props: {} }; }; @@ -106,9 +106,9 @@ import { cookies } from 'next/headers'; const ServerComponent = async () => { // Read-only operations in Server Components - const value = getCookie('test', { cookies }); - const allCookies = getCookies({ cookies }); - const exists = hasCookie('test', { cookies }); + const value = await getCookie('test', { cookies }); + const allCookies = await getCookies({ cookies }); + const exists = await hasCookie('test', { cookies }); // Note: setCookie and deleteCookie cannot be used in Server Components return
...
; @@ -124,11 +124,11 @@ import { cookies } from 'next/headers'; import { setCookie, deleteCookie, getCookie, getCookies, hasCookie } from 'cookies-next'; export async function serverAction() { - setCookie('test', 'value', { cookies }); - deleteCookie('test', { cookies }); - getCookie('test', { cookies }); - getCookies({ cookies }); - hasCookie('test', { cookies }); + await setCookie('test', 'value', { cookies }); + await deleteCookie('test', { cookies }); + await getCookie('test', { cookies }); + await getCookies({ cookies }); + await hasCookie('test', { cookies }); } ``` @@ -138,10 +138,10 @@ export async function serverAction() { import { getCookies, getCookie, setCookie, deleteCookie } from 'cookies-next'; export default async function handler(req, res) { - setCookie('key', 'value', { req, res, maxAge: 60 * 60 * 24 }); - getCookie('key', { req, res }); - getCookies({ req, res }); - deleteCookie('key', { req, res }); + await setCookie('key', 'value', { req, res, maxAge: 60 * 60 * 24 }); + await getCookie('key', { req, res }); + await getCookies({ req, res }); + await deleteCookie('key', { req, res }); return res.status(200).json({ message: 'ok' }); } @@ -156,18 +156,18 @@ import { deleteCookie, getCookie, setCookie, hasCookie, getCookies } from 'cooki export async function GET(req: NextRequest) { const res = new NextResponse(); - setCookie('test', 'value', { res, req }); - getCookie('test', { res, req }); - getCookies({ res, req }); - deleteCookie('test', { res, req }); - hasCookie('test', { req, res }); + await setCookie('test', 'value', { res, req }); + await getCookie('test', { res, req }); + await getCookies({ res, req }); + await deleteCookie('test', { res, req }); + await hasCookie('test', { req, res }); // Using cookies function - setCookie('test1', 'value', { cookies }); - getCookie('test1', { cookies }); - getCookies({ cookies }); - deleteCookie('test1', { cookies }); - hasCookie('test1', { cookies }); + await setCookie('test1', 'value', { cookies }); + await getCookie('test1', { cookies }); + await getCookies({ cookies }); + await deleteCookie('test1', { cookies }); + await hasCookie('test1', { cookies }); return res; } @@ -180,13 +180,13 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { getCookie, setCookie, deleteCookie, hasCookie, getCookies } from 'cookies-next'; -export function middleware(req: NextRequest) { +export async function middleware(req: NextRequest) { const res = NextResponse.next(); - setCookie('test', 'value', { res, req }); - hasCookie('test', { req, res }); - deleteCookie('test', { res, req }); - getCookie('test', { res, req }); - getCookies({ res, req }); + await setCookie('test', 'value', { res, req }); + await hasCookie('test', { req, res }); + await deleteCookie('test', { res, req }); + await getCookie('test', { res, req }); + await getCookies({ res, req }); // Note: cookies function from next/headers cannot be used in middleware return res; @@ -197,23 +197,43 @@ export function middleware(req: NextRequest) { ### setCookie(key, value, options) -Sets a cookie. +Sets a cookie. Returns a Promise. + +```javascript +await setCookie('key', 'value', options); +``` ### getCookie(key, options) -Retrieves a specific cookie. +Retrieves a specific cookie. Returns a Promise. + +```javascript +const value = await getCookie('key', options); +``` ### getCookies(options) -Retrieves all cookies. +Retrieves all cookies. Returns a Promise. + +```javascript +const cookies = await getCookies(options); +``` ### hasCookie(key, options) -Checks if a cookie exists. +Checks if a cookie exists. Returns a Promise. + +```javascript +const exists = await hasCookie('key', options); +``` ### deleteCookie(key, options) -Deletes a cookie. +Deletes a cookie. Returns a Promise. + +```javascript +await deleteCookie('key', options); +``` ## Options From a7ededd7ac948192caaad46f701ce44b48613acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 09:56:26 -0300 Subject: [PATCH 06/30] refactor: update cookie package and adjust import for serialization options --- package.json | 2 +- src/types.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ed77e4b..4cded4d 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "homepage": "https://github.com/andreizanik/cookies-next#readme", "dependencies": { "@types/cookie": "^0.6.0", - "cookie": "^0.7.0" + "cookie": "^1.0.1" }, "devDependencies": { "@types/jest": "^29.5.13", diff --git a/src/types.ts b/src/types.ts index e30c714..2cf647a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,10 @@ -import { CookieSerializeOptions } from 'cookie'; +import { SerializeOptions } from 'cookie'; import { IncomingMessage, ServerResponse } from 'http'; import type { NextRequest, NextResponse } from 'next/server'; import type { cookies } from 'next/headers'; export type OptionsType = DefaultOptions | AppRouterOptions; -export interface DefaultOptions extends CookieSerializeOptions { +export interface DefaultOptions extends SerializeOptions { res?: ServerResponse; req?: IncomingMessage & { cookies?: TmpCookiesObj; From fd3f62b27ab0101c08d9ae8f6f7e993267f3a321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 09:58:21 -0300 Subject: [PATCH 07/30] test(cookies): change beforeEach to afterEach to clear mocks after tests --- src/cookies.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cookies.test.ts b/src/cookies.test.ts index 8ea04fe..8d778c2 100644 --- a/src/cookies.test.ts +++ b/src/cookies.test.ts @@ -28,7 +28,7 @@ const mockOptions = { } satisfies OptionsType; describe('Cookie Functions', () => { - beforeEach(() => { + afterEach(() => { jest.clearAllMocks(); // Reset document.cookie before each test From 710d58fa856bb219cbaaffb6d3df11482dbe5ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 10:06:56 -0300 Subject: [PATCH 08/30] docs: clarify middleware cookie handling in readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e006e3a..6262e60 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,8 @@ export async function middleware(req: NextRequest) { await getCookies({ res, req }); // Note: cookies function from next/headers cannot be used in middleware + ❌ setCookie('test', 'value', { cookies }); // 👉🏻 Won't work. + return res; } ``` From aede22e4fd0747ef8a9ef5ea70cfd217e58da956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 10:13:54 -0300 Subject: [PATCH 09/30] docs: update readme with next.js version compatibility and usage examples --- README.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6262e60..978b30d 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,16 @@ A versatile cookie management library for Next.js applications, supporting both ## Installation +For Next.js versions 15 and above, use the latest version of cookies-next. + ```bash -npm install --save cookies-next +npm install --save cookies-next@latest ``` -For Next.js versions 15 and above, use the latest version of cookies-next. - For Next.js versions 12.2.0 to 13.x, use cookies-next version 4.3.0: ```bash -npm install --save cookies-next@5.0.0 +npm install --save cookies-next@4.3.0 ``` ## Usage @@ -70,6 +70,7 @@ await deleteCookie('key', options); ```javascript 'use client'; + import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next'; // Use anywhere in client-side code @@ -104,13 +105,20 @@ In Server Components: import { getCookie, getCookies, hasCookie } from 'cookies-next'; import { cookies } from 'next/headers'; -const ServerComponent = async () => { +export const ServerComponent = async () => { // Read-only operations in Server Components const value = await getCookie('test', { cookies }); const allCookies = await getCookies({ cookies }); const exists = await hasCookie('test', { cookies }); - // Note: setCookie and deleteCookie cannot be used in Server Components + /** + * Note: It's not possible to update the cookie in RSC. + * + * `setCookie` and `deleteCookie` cannot be used in Server Components + */ + ❌ setCookie("test", "value", { cookies }); // 👉🏻 Won't work. + ❌ deleteCookie('test1', { cookies }); // 👉🏻 Won't work. + return
...
; }; ``` @@ -135,13 +143,14 @@ export async function serverAction() { ### API Routes (Pages Router) ```javascript -import { getCookies, getCookie, setCookie, deleteCookie } from 'cookies-next'; +import { getCookies, getCookie, setCookie, deleteCookie, hasCookie } from 'cookies-next'; export default async function handler(req, res) { await setCookie('key', 'value', { req, res, maxAge: 60 * 60 * 24 }); await getCookie('key', { req, res }); await getCookies({ req, res }); await deleteCookie('key', { req, res }); + await hasCookie('key', { req, res }); return res.status(200).json({ message: 'ok' }); } From 078896b006098def6fde353f64896eb3cddf65a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 12:09:38 -0300 Subject: [PATCH 10/30] refactor: restructure codebase to separate client and server logic, update package.json, split tests --- README.md | 2 +- package.json | 23 ++++- src/client/client.test.ts | 53 ++++++++++ src/client/index.ts | 54 +++++++++++ src/common/types.ts | 23 +++++ src/common/utils.ts | 18 ++++ src/cookies.test.ts | 145 ---------------------------- src/index.ts | 198 ++------------------------------------ src/server/index.ts | 169 ++++++++++++++++++++++++++++++++ src/types.ts | 8 ++ 10 files changed, 351 insertions(+), 342 deletions(-) create mode 100644 src/client/client.test.ts create mode 100644 src/client/index.ts create mode 100644 src/common/types.ts create mode 100644 src/common/utils.ts delete mode 100644 src/cookies.test.ts create mode 100644 src/server/index.ts diff --git a/README.md b/README.md index 978b30d..16df5d4 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ export const ServerComponent = async () => { /** * Note: It's not possible to update the cookie in RSC. - * + * * `setCookie` and `deleteCookie` cannot be used in Server Components */ ❌ setCookie("test", "value", { cookies }); // 👉🏻 Won't work. diff --git a/package.json b/package.json index 4cded4d..3dc0143 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,24 @@ { "name": "cookies-next", - "version": "5.0.0", - "description": "Getting, setting and removing cookies on both client and server with next.js", - "main": "lib/index.js", - "type": "lib/index.d.ts", + "version": "2.1.1", + "description": "Set, Get, Remove cookies on both client and server side with Next.js", + "main": "./lib/index.js", + "exports": { + ".": "./lib/index.js", + "./client": "./lib/client/index.js", + "./server": "./lib/server/index.js" + }, + "types": "./lib/index.d.ts", + "typesVersions": { + "*": { + "client": [ + "./lib/client/index.d.ts" + ], + "server": [ + "./lib/server/index.d.ts" + ] + } + }, "scripts": { "build": "tsc", "pretty": "npx prettier . --write", diff --git a/src/client/client.test.ts b/src/client/client.test.ts new file mode 100644 index 0000000..642dd45 --- /dev/null +++ b/src/client/client.test.ts @@ -0,0 +1,53 @@ +import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from '../index'; + +// Mock the window object +// const windowSpy = jest.spyOn(global, 'window', 'get'); + +describe('Client-side cookie operations', () => { + test('getCookies should return all cookies', () => { + setCookie('key1', 'value1'); + setCookie('key2', 'value2'); + const cookies = getCookies(); + expect(cookies).toEqual({ key1: 'value1', key2: 'value2' }); + }); + + test('setCookie should set a cookie', () => { + setCookie('testKey', 'testValue'); + expect(document.cookie).toContain('testKey=testValue'); + }); + + test('getCookie should retrieve a set cookie', () => { + document.cookie = 'testKey2=testValue2'; + const value = getCookie('testKey2'); + expect(value).toBe('testValue2'); + }); + + test('deleteCookie should remove a cookie', () => { + document.cookie = 'testKey3=testValue3'; + deleteCookie('testKey3'); + expect(document.cookie).not.toContain('testKey3=testValue3'); + }); + + test('hasCookie should return true for existing cookie', () => { + document.cookie = 'testKey4=testValue4'; + const exists = hasCookie('testKey4'); + expect(exists).toBe(true); + }); + + test('hasCookie should return false for non-existing cookie', () => { + const exists = hasCookie('nonExistentKey5'); + expect(exists).toBe(false); + }); + + test('getCookie should return undefined for non-existing cookie', () => { + const value = getCookie('nonExistentKey'); + expect(value).toBeUndefined(); + }); + + test('setCookie should handle complex values', () => { + const complexValue = { key: 'value', nested: { array: [1, 2, 3] } }; + setCookie('complexKey', complexValue); + const retrievedValue = getCookie('complexKey'); + expect(JSON.parse(retrievedValue as string)).toEqual(complexValue); + }); +}); diff --git a/src/client/index.ts b/src/client/index.ts new file mode 100644 index 0000000..769bdd8 --- /dev/null +++ b/src/client/index.ts @@ -0,0 +1,54 @@ +import { serialize, parse } from 'cookie'; +import type { OptionsType, TmpCookiesObj, CookieValueTypes } from '../common/types'; +import { stringify, decode, isClientSide } from '../common/utils'; + +const ensureClientSide = () => { + if (!isClientSide()) { + throw new Error('You are trying to access cookies on the server side. Please, use the server-side import with `cookies-next/server` instead.'); + } +}; + +const getCookies = (_options?: OptionsType): TmpCookiesObj => { + ensureClientSide(); + const cookies: TmpCookiesObj = {}; + const documentCookies = document.cookie ? document.cookie.split('; ') : []; + + for (let i = 0, len = documentCookies.length; i < len; i++) { + const cookieParts = documentCookies[i].split('='); + const cookie = cookieParts.slice(1).join('='); + const name = cookieParts[0]; + cookies[name] = cookie; + } + + return cookies; +}; + +const getCookie = (key: string, options?: OptionsType): CookieValueTypes => { + ensureClientSide(); + const _cookies = getCookies(options); + const value = _cookies[key]; + if (value === undefined) return undefined; + return decode(value); +}; + +const setCookie = (key: string, data: any, options?: OptionsType): void => { + ensureClientSide(); + const _cookieOptions = options || {}; + const cookieStr = serialize(key, stringify(data), { path: '/', ..._cookieOptions }); + document.cookie = cookieStr; +}; + +const deleteCookie = (key: string, options?: OptionsType): void => { + ensureClientSide(); + setCookie(key, '', { ...options, maxAge: -1 }); +}; + +const hasCookie = (key: string, options?: OptionsType): boolean => { + ensureClientSide(); + if (!key) return false; + const cookies = getCookies(options); + return Object.prototype.hasOwnProperty.call(cookies, key); +}; + +export * from '../common/types'; +export { getCookies, getCookie, setCookie, deleteCookie, hasCookie }; diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 0000000..2cf647a --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,23 @@ +import { SerializeOptions } from 'cookie'; +import { IncomingMessage, ServerResponse } from 'http'; +import type { NextRequest, NextResponse } from 'next/server'; +import type { cookies } from 'next/headers'; + +export type OptionsType = DefaultOptions | AppRouterOptions; +export interface DefaultOptions extends SerializeOptions { + res?: ServerResponse; + req?: IncomingMessage & { + cookies?: TmpCookiesObj; + }; + cookies?: CookiesFn; +} + +export type CookiesFn = () => ReturnType; +export type AppRouterOptions = { + res?: Response | NextResponse; + req?: Request | NextRequest; + cookies?: CookiesFn; +}; +export type AppRouterCookies = NextResponse['cookies'] | NextRequest['cookies']; +export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>; +export type CookieValueTypes = string | undefined; diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 0000000..4c7082a --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,18 @@ +export const stringify = (value: any) => { + try { + if (typeof value === 'string') { + return value; + } + const stringifiedValue = JSON.stringify(value); + return stringifiedValue; + } catch (e) { + return value; + } +}; + +export const decode = (str: string): string => { + if (!str) return str; + return str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); +}; + +export const isClientSide = () => typeof window !== 'undefined'; diff --git a/src/cookies.test.ts b/src/cookies.test.ts deleted file mode 100644 index 8d778c2..0000000 --- a/src/cookies.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { getCookies, getCookie, setCookie, deleteCookie, hasCookie, validateContextCookies } from './index'; -import type { OptionsType, AppRouterCookies } from './types'; -import { NextRequest, NextResponse } from 'next/server'; - -// Mock implementations -const mockAppRouterCookies: AppRouterCookies = { - getAll: jest.fn().mockReturnValue([ - { name: 'test', value: 'value' }, - { name: 'jsonCookie', value: JSON.stringify({ foo: 'bar' }) } - ]), - set: jest.fn(), - get: jest.fn(), - has: jest.fn(), - delete: jest.fn(), - clear: jest.fn(), - [Symbol.iterator]: jest.fn(), - size: 2, -}; - -const mockOptions = { - req: { - cookies: mockAppRouterCookies, - } as unknown as NextRequest, - res: { - cookies: mockAppRouterCookies, - } as unknown as NextResponse, - cookies: jest.fn().mockResolvedValue(mockAppRouterCookies), -} satisfies OptionsType; - -describe('Cookie Functions', () => { - afterEach(() => { - jest.clearAllMocks(); - - // Reset document.cookie before each test - Object.defineProperty(document, 'cookie', { - writable: true, - value: '', - }); - }); - - describe('validateContextCookies', () => { - test('should return true for valid AppRouterCookies', async () => { - const result = await validateContextCookies(mockOptions); - expect(result).toBe(true); - }); - - test('should return false for invalid context', async () => { - const result = await validateContextCookies({}); - expect(result).toBe(false); - }); - }); - - describe('getCookies', () => { - test('should return transformed cookies for valid context', async () => { - const cookies = await getCookies(mockOptions); - expect(cookies).toEqual({ test: 'value', jsonCookie: JSON.stringify({ foo: 'bar' }) }); - }); - - test('should return empty object for client-side without cookies', async () => { - const cookies = await getCookies(); - expect(cookies).toEqual({}); - }); - - test('should parse client-side cookies', async () => { - document.cookie = 'key1=value1; key2=value2'; - const cookies = await getCookies(); - expect(cookies).toEqual({ key1: 'value1', key2: 'value2' }); - }); - }); - - describe('getCookie', () => { - test('should return specific cookie value for valid context', async () => { - const value = await getCookie('test', mockOptions); - expect(value).toBe('value'); - }); - - test('should return undefined for non-existent cookie', async () => { - const value = await getCookie('nonexistent', mockOptions); - expect(value).toBeUndefined(); - }); - - test('should parse JSON cookie value', async () => { - const value = await getCookie('jsonCookie', mockOptions); - expect(value).toBe('{"foo":"bar"}'); - }); - }); - - describe('setCookie', () => { - test('should set a cookie in AppRouterCookies', async () => { - await setCookie('newKey', 'newValue', mockOptions); - expect(mockAppRouterCookies.set).toHaveBeenCalledWith({ - name: 'newKey', - value: 'newValue', - }); - }); - - test('should set a cookie on client-side', async () => { - await setCookie('clientKey', 'clientValue'); - expect(document.cookie).toContain('clientKey=clientValue'); - }); - - test('should stringify non-string values', async () => { - await setCookie('objectKey', { foo: 'bar' }, mockOptions); - expect(mockAppRouterCookies.set).toHaveBeenCalledWith({ - name: 'objectKey', - value: '{"foo":"bar"}', - }); - }); - }); - - describe('deleteCookie', () => { - test('should delete a cookie in AppRouterCookies', async () => { - await deleteCookie('test', mockOptions); - expect(mockAppRouterCookies.set).toHaveBeenCalledWith({ - name: 'test', - value: '', - maxAge: -1, - }); - }); - - test('should delete a cookie on client-side', async () => { - document.cookie = 'toDelete=value; path=/'; - await deleteCookie('toDelete'); - expect(document.cookie).not.toContain('toDelete=value'); - }); - }); - - describe('hasCookie', () => { - test('should return true for existing cookie in AppRouterCookies', async () => { - const result = await hasCookie('test', mockOptions); - expect(result).toBe(true); - }); - - test('should return false for non-existent cookie', async () => { - const result = await hasCookie('nonexistent', mockOptions); - expect(result).toBe(false); - }); - - test('should check for cookie existence on client-side', async () => { - document.cookie = 'existingCookie=value; path=/'; - const result = await hasCookie('existingCookie'); - expect(result).toBe(true); - }); - }); -}); diff --git a/src/index.ts b/src/index.ts index a1eb8df..ed05d0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,194 +1,8 @@ -import { serialize, parse } from 'cookie'; -import type { - OptionsType, - TmpCookiesObj, - CookieValueTypes, - AppRouterCookies, - DefaultOptions, - CookiesFn, -} from './types'; -import type { NextRequest, NextResponse } from 'next/server'; -export { CookieValueTypes } from './types'; +import * as clientCookies from './client'; +import * as serverCookies from './server'; +export * from './common/types'; -const isClientSide = (): boolean => typeof window !== 'undefined'; +// Re-export individual functions for backwards compatibility +export const { getCookie, getCookies, setCookie, deleteCookie, hasCookie } = + typeof window === 'undefined' ? serverCookies : clientCookies; -const isCookiesFromAppRouter = ( - cookieStore: TmpCookiesObj | AppRouterCookies | undefined, -): cookieStore is AppRouterCookies => { - if (!cookieStore) return false; - return ( - 'getAll' in cookieStore && - 'set' in cookieStore && - typeof cookieStore.getAll === 'function' && - typeof cookieStore.set === 'function' - ); -}; - -const isPotentialContextFromAppRouter = ( - context?: OptionsType, -): context is { res?: NextResponse; req?: NextRequest; cookies?: CookiesFn } => { - return ( - (!!context?.req && 'cookies' in context.req) || - (!!context?.res && 'cookies' in context.res) || - (!!context?.cookies && typeof context.cookies === 'function') - ); -}; - -export const validateContextCookies = async (context: { - res?: NextResponse; - req?: NextRequest; - cookies?: CookiesFn; -}): Promise => { - if (context.req && 'cookies' in context.req) { - return isCookiesFromAppRouter(context.req.cookies); - } - if (context.res && 'cookies' in context.res) { - return isCookiesFromAppRouter(context.res.cookies); - } - if (context.cookies) { - const cookies = await context.cookies(); - return isCookiesFromAppRouter(cookies); - } - return false; -}; - -const transformAppRouterCookies = (cookies: AppRouterCookies): TmpCookiesObj => { - let _cookies: Partial = {}; - - cookies.getAll().forEach(({ name, value }) => { - _cookies[name] = value; - }); - return _cookies; -}; - -const stringify = (value: any) => { - try { - if (typeof value === 'string') { - return value; - } - const stringifiedValue = JSON.stringify(value); - return stringifiedValue; - } catch (e) { - return value; - } -}; - -const decode = (str: string): string => { - if (!str) return str; - - return str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); -}; - -export const getCookies = async (options?: OptionsType): Promise => { - if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { - if (options?.req) { - return transformAppRouterCookies(options.req.cookies); - } - if (options?.cookies) { - if (await validateContextCookies({ cookies: options.cookies })) { - return transformAppRouterCookies(await options.cookies()); - } - } - } - - let req; - // DefaultOptions['req] can be casted here because is narrowed by using the fn: isPotentialContextFromAppRouter - if (options) req = options.req as DefaultOptions['req']; - - if (!isClientSide()) { - // if cookie-parser is used in project get cookies from ctx.req.cookies - // if cookie-parser isn't used in project get cookies from ctx.req.headers.cookie - - if (req && req.cookies) return req.cookies; - if (req && req.headers.cookie) return parse(req.headers.cookie); - return {}; - } - - const _cookies: TmpCookiesObj = {}; - const documentCookies = document.cookie ? document.cookie.split('; ') : []; - - for (let i = 0, len = documentCookies.length; i < len; i++) { - const cookieParts = documentCookies[i].split('='); - - const _cookie = cookieParts.slice(1).join('='); - const name = cookieParts[0]; - - _cookies[name] = _cookie; - } - - return _cookies; -}; - -export const getCookie = async (key: string, options?: OptionsType): Promise => { - const _cookies = await getCookies(options); - const value = _cookies[key]; - if (value === undefined) return undefined; - return decode(value); -}; - -export const setCookie = async (key: string, data: any, options?: OptionsType): Promise => { - if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { - const { req, res, cookies: cookiesFn, ...restOptions } = options; - const payload = { name: key, value: stringify(data), ...restOptions }; - - if (req) { - req.cookies.set(payload); - } - if (res) { - res.cookies.set(payload); - } - if (cookiesFn) { - (await cookiesFn()).set(payload); - } - return; - } - let _cookieOptions: any; - let _req; - let _res; - if (options) { - // DefaultOptions can be casted here because the AppRouterMiddlewareOptions is narrowed using the fn: isContextFromAppRouter - const { req, res, ..._options } = options as DefaultOptions; - _req = req; - _res = res; - _cookieOptions = _options; - } - - const cookieStr = serialize(key, stringify(data), { path: '/', ..._cookieOptions }); - if (!isClientSide()) { - if (_res && _req) { - let currentCookies = _res.getHeader('Set-Cookie'); - - if (!Array.isArray(currentCookies)) { - currentCookies = !currentCookies ? [] : [String(currentCookies)]; - } - _res.setHeader('Set-Cookie', currentCookies.concat(cookieStr)); - - if (_req && _req.cookies) { - const _cookies = _req.cookies; - data === '' ? delete _cookies[key] : (_cookies[key] = stringify(data)); - } - - if (_req && _req.headers && _req.headers.cookie) { - const _cookies = parse(_req.headers.cookie); - - data === '' ? delete _cookies[key] : (_cookies[key] = stringify(data)); - - _req.headers.cookie = Object.entries(_cookies).reduce((accum, item) => { - return accum.concat(`${item[0]}=${item[1]};`); - }, ''); - } - } - } else { - document.cookie = cookieStr; - } -}; - -export const deleteCookie = async (key: string, options?: OptionsType): Promise => { - return setCookie(key, '', { ...options, maxAge: -1 }); -}; - -export const hasCookie = async (key: string, options?: OptionsType): Promise => { - if (!key) return false; - const cookie = await getCookies(options); - return cookie.hasOwnProperty(key); -}; diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..abb65a7 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,169 @@ +import { serialize, parse } from 'cookie'; +import type { + OptionsType, + TmpCookiesObj, + CookieValueTypes, + AppRouterCookies, + DefaultOptions, + CookiesFn, +} from '../common/types'; +import { stringify, decode, isClientSide } from '../common/utils'; +import type { NextRequest, NextResponse } from 'next/server'; + +const ensureServerSide = () => { + if (isClientSide()) { + throw new Error('You are trying to access cookies on the client side. Please, use the client-side import with `cookies-next/client` instead.'); + } +}; + +const isCookiesFromAppRouter = ( + cookieStore: TmpCookiesObj | AppRouterCookies | undefined, +): cookieStore is AppRouterCookies => { + ensureServerSide(); + if (!cookieStore) return false; + return ( + 'getAll' in cookieStore && + 'set' in cookieStore && + typeof cookieStore.getAll === 'function' && + typeof cookieStore.set === 'function' + ); +}; + +const isPotentialContextFromAppRouter = ( + context?: OptionsType, +): context is { res?: NextResponse; req?: NextRequest; cookies?: CookiesFn } => { + ensureServerSide(); + return ( + (!!context?.req && 'cookies' in context.req) || + (!!context?.res && 'cookies' in context.res) || + (!!context?.cookies && typeof context.cookies === 'function') + ); +}; + +const validateContextCookies = async (context: { + res?: NextResponse; + req?: NextRequest; + cookies?: CookiesFn; +}): Promise => { + ensureServerSide(); + if (context.req && 'cookies' in context.req) { + return isCookiesFromAppRouter(context.req.cookies); + } + if (context.res && 'cookies' in context.res) { + return isCookiesFromAppRouter(context.res.cookies); + } + if (context.cookies) { + const cookies = await context.cookies(); + return isCookiesFromAppRouter(cookies); + } + return false; +}; + +const transformAppRouterCookies = (cookies: AppRouterCookies): TmpCookiesObj => { + ensureServerSide(); + let _cookies: Partial = {}; + cookies.getAll().forEach(({ name, value }) => { + _cookies[name] = value; + }); + return _cookies; +}; + +const getCookies = async (options?: OptionsType): Promise => { + ensureServerSide(); + + if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { + if (options?.req) { + return transformAppRouterCookies(options.req.cookies); + } + if (options?.cookies) { + if (await validateContextCookies({ cookies: options.cookies })) { + return transformAppRouterCookies(await options.cookies()); + } + } + } + + let req; + if (options) req = options.req as DefaultOptions['req']; + if (req && req.cookies) return req.cookies; + if (req && req.headers.cookie) return parse(req.headers.cookie); + return {}; +}; + +const getCookie = async (key: string, options?: OptionsType): Promise => { + ensureServerSide(); + const _cookies = await getCookies(options); + const value = _cookies[key]; + if (value === undefined) return undefined; + return decode(value); +}; + +const setCookie = async (key: string, data: any, options?: OptionsType): Promise => { + ensureServerSide(); + + if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { + const { req, res, cookies: cookiesFn, ...restOptions } = options; + const payload = { name: key, value: stringify(data), ...restOptions }; + + if (req) { + req.cookies.set(payload); + } + if (res) { + res.cookies.set(payload); + } + if (cookiesFn) { + (await cookiesFn()).set(payload); + } + return; + } + + let _cookieOptions: any; + let _req; + let _res; + + if (options) { + const { req, res, ..._options } = options as DefaultOptions; + _req = req; + _res = res; + _cookieOptions = _options; + } + + const cookieStr = serialize(key, stringify(data), { path: '/', ..._cookieOptions }); + if (_res && _req) { + let currentCookies = _res.getHeader('Set-Cookie'); + + if (!Array.isArray(currentCookies)) { + currentCookies = !currentCookies ? [] : [String(currentCookies)]; + } + _res.setHeader('Set-Cookie', currentCookies.concat(cookieStr)); + + if (_req && _req.cookies) { + const _cookies = _req.cookies; + data === '' ? delete _cookies[key] : (_cookies[key] = stringify(data)); + } + + if (_req && _req.headers && _req.headers.cookie) { + const _cookies = parse(_req.headers.cookie); + + data === '' ? delete _cookies[key] : (_cookies[key] = stringify(data)); + + _req.headers.cookie = Object.entries(_cookies).reduce((accum, item) => { + return accum.concat(`${item[0]}=${item[1]};`); + }, ''); + } + } +}; + +const deleteCookie = async (key: string, options?: OptionsType): Promise => { + ensureServerSide(); + return setCookie(key, '', { ...options, maxAge: -1 }); +}; + +const hasCookie = async (key: string, options?: OptionsType): Promise => { + ensureServerSide(); + if (!key) return false; + const cookie = await getCookies(options); + return cookie.hasOwnProperty(key); +}; + +export * from '../common/types'; +export { getCookies, getCookie, setCookie, deleteCookie, hasCookie }; diff --git a/src/types.ts b/src/types.ts index 2cf647a..889db9b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,3 +21,11 @@ export type AppRouterOptions = { export type AppRouterCookies = NextResponse['cookies'] | NextRequest['cookies']; export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>; export type CookieValueTypes = string | undefined; + +export interface SyncFunctions { + getCookies: (options?: OptionsType) => TmpCookiesObj; + getCookie: (key: string, options?: OptionsType) => CookieValueTypes; + setCookie: (key: string, data: any, options?: OptionsType) => void; + deleteCookie: (key: string, options?: OptionsType) => void; + hasCookie: (key: string, options?: OptionsType) => boolean; +} From 8eb9ea7d98544b85278598677da2ca5e130d02fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 12:09:51 -0300 Subject: [PATCH 11/30] refactor: remove unused parse import from cookie module --- src/client/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/index.ts b/src/client/index.ts index 769bdd8..7b1ec2c 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,4 +1,4 @@ -import { serialize, parse } from 'cookie'; +import { serialize } from 'cookie'; import type { OptionsType, TmpCookiesObj, CookieValueTypes } from '../common/types'; import { stringify, decode, isClientSide } from '../common/utils'; From bd8ca0a7768ae97befd34061ab72d3c4e1ec8fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 17:52:29 -0300 Subject: [PATCH 12/30] refactor: update context types and improve cookie handling logic - replaced and with and for better clarity and alignment with Next.js conventions - simplified cookie transformation and validation functions to use - removed redundant type definitions and moved relevant types to a centralized file - improved error handling by formatting error messages for better readability - deleted unused file to reduce code duplication and maintain consistency across the codebase --- src/client/client.test.ts | 5 +- src/client/index.ts | 4 +- src/common/types.ts | 20 ++-- src/index.ts | 1 - src/server/index.ts | 125 +++++++++++------------- src/server/server.test.ts | 194 ++++++++++++++++++++++++++++++++++++++ src/types.ts | 31 ------ 7 files changed, 264 insertions(+), 116 deletions(-) create mode 100644 src/server/server.test.ts delete mode 100644 src/types.ts diff --git a/src/client/client.test.ts b/src/client/client.test.ts index 642dd45..b6f1013 100644 --- a/src/client/client.test.ts +++ b/src/client/client.test.ts @@ -1,8 +1,5 @@ import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from '../index'; -// Mock the window object -// const windowSpy = jest.spyOn(global, 'window', 'get'); - describe('Client-side cookie operations', () => { test('getCookies should return all cookies', () => { setCookie('key1', 'value1'); @@ -10,7 +7,7 @@ describe('Client-side cookie operations', () => { const cookies = getCookies(); expect(cookies).toEqual({ key1: 'value1', key2: 'value2' }); }); - + test('setCookie should set a cookie', () => { setCookie('testKey', 'testValue'); expect(document.cookie).toContain('testKey=testValue'); diff --git a/src/client/index.ts b/src/client/index.ts index 7b1ec2c..ea39e14 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -4,7 +4,9 @@ import { stringify, decode, isClientSide } from '../common/utils'; const ensureClientSide = () => { if (!isClientSide()) { - throw new Error('You are trying to access cookies on the server side. Please, use the server-side import with `cookies-next/server` instead.'); + throw new Error( + 'You are trying to access cookies on the server side. Please, use the server-side import with `cookies-next/server` instead.', + ); } }; diff --git a/src/common/types.ts b/src/common/types.ts index 2cf647a..aad8135 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -3,21 +3,21 @@ import { IncomingMessage, ServerResponse } from 'http'; import type { NextRequest, NextResponse } from 'next/server'; import type { cookies } from 'next/headers'; -export type OptionsType = DefaultOptions | AppRouterOptions; -export interface DefaultOptions extends SerializeOptions { - res?: ServerResponse; +export interface HttpContext extends SerializeOptions { req?: IncomingMessage & { + // Might be set by third-party libraries such as `cookie-parser` cookies?: TmpCookiesObj; }; + res?: ServerResponse; +} +export interface NextContext { + req?: NextRequest; + res?: NextResponse; cookies?: CookiesFn; } +export type OptionsType = HttpContext | NextContext; -export type CookiesFn = () => ReturnType; -export type AppRouterOptions = { - res?: Response | NextResponse; - req?: Request | NextRequest; - cookies?: CookiesFn; -}; -export type AppRouterCookies = NextResponse['cookies'] | NextRequest['cookies']; +export type CookiesFn = typeof cookies; +export type NextCookies = NextResponse['cookies'] | NextRequest['cookies']; export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>; export type CookieValueTypes = string | undefined; diff --git a/src/index.ts b/src/index.ts index ed05d0a..9581d94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,3 @@ export * from './common/types'; // Re-export individual functions for backwards compatibility export const { getCookie, getCookies, setCookie, deleteCookie, hasCookie } = typeof window === 'undefined' ? serverCookies : clientCookies; - diff --git a/src/server/index.ts b/src/server/index.ts index abb65a7..db1a0e1 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,25 +1,23 @@ -import { serialize, parse } from 'cookie'; +import { serialize, parse, SerializeOptions } from 'cookie'; import type { OptionsType, TmpCookiesObj, CookieValueTypes, - AppRouterCookies, - DefaultOptions, - CookiesFn, + NextCookies, + HttpContext, + NextContext, } from '../common/types'; import { stringify, decode, isClientSide } from '../common/utils'; -import type { NextRequest, NextResponse } from 'next/server'; const ensureServerSide = () => { if (isClientSide()) { - throw new Error('You are trying to access cookies on the client side. Please, use the client-side import with `cookies-next/client` instead.'); + throw new Error( + 'You are trying to access cookies on the client side. Please, use the client-side import with `cookies-next/client` instead.', + ); } }; -const isCookiesFromAppRouter = ( - cookieStore: TmpCookiesObj | AppRouterCookies | undefined, -): cookieStore is AppRouterCookies => { - ensureServerSide(); +const isCookiesFromNext = (cookieStore: TmpCookiesObj | NextCookies | undefined): cookieStore is NextCookies => { if (!cookieStore) return false; return ( 'getAll' in cookieStore && @@ -29,37 +27,15 @@ const isCookiesFromAppRouter = ( ); }; -const isPotentialContextFromAppRouter = ( - context?: OptionsType, -): context is { res?: NextResponse; req?: NextRequest; cookies?: CookiesFn } => { - ensureServerSide(); +const isContextFromNext = (context?: OptionsType): context is NextContext => { return ( - (!!context?.req && 'cookies' in context.req) || - (!!context?.res && 'cookies' in context.res) || - (!!context?.cookies && typeof context.cookies === 'function') + (!!context?.req && 'cookies' in context.req && isCookiesFromNext(context.req.cookies)) || + (!!context?.res && 'cookies' in context.res && isCookiesFromNext(context.res.cookies)) || + (!!context && 'cookies' in context && typeof context.cookies === 'function') ); }; -const validateContextCookies = async (context: { - res?: NextResponse; - req?: NextRequest; - cookies?: CookiesFn; -}): Promise => { - ensureServerSide(); - if (context.req && 'cookies' in context.req) { - return isCookiesFromAppRouter(context.req.cookies); - } - if (context.res && 'cookies' in context.res) { - return isCookiesFromAppRouter(context.res.cookies); - } - if (context.cookies) { - const cookies = await context.cookies(); - return isCookiesFromAppRouter(cookies); - } - return false; -}; - -const transformAppRouterCookies = (cookies: AppRouterCookies): TmpCookiesObj => { +const transformAppRouterCookies = (cookies: NextCookies): TmpCookiesObj => { ensureServerSide(); let _cookies: Partial = {}; cookies.getAll().forEach(({ name, value }) => { @@ -71,28 +47,40 @@ const transformAppRouterCookies = (cookies: AppRouterCookies): TmpCookiesObj => const getCookies = async (options?: OptionsType): Promise => { ensureServerSide(); - if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { - if (options?.req) { + // Use Next.js context if available + if (isContextFromNext(options)) { + if (options.req) { return transformAppRouterCookies(options.req.cookies); } - if (options?.cookies) { - if (await validateContextCookies({ cookies: options.cookies })) { - return transformAppRouterCookies(await options.cookies()); - } + if (options.res) { + return transformAppRouterCookies(options.res.cookies); } + if (options.cookies) { + return transformAppRouterCookies(await options.cookies()); + } + } + + // Use context from the default HTTP request context + let httpRequest; + if (options?.req) { + httpRequest = options.req as HttpContext['req']; + } + + if (httpRequest?.cookies) { + return httpRequest.cookies; + } + + if (httpRequest?.headers.cookie) { + return parse(httpRequest.headers.cookie); } - let req; - if (options) req = options.req as DefaultOptions['req']; - if (req && req.cookies) return req.cookies; - if (req && req.headers.cookie) return parse(req.headers.cookie); return {}; }; const getCookie = async (key: string, options?: OptionsType): Promise => { ensureServerSide(); - const _cookies = await getCookies(options); - const value = _cookies[key]; + const cookies = await getCookies(options); + const value = cookies[key]; if (value === undefined) return undefined; return decode(value); }; @@ -100,7 +88,7 @@ const getCookie = async (key: string, options?: OptionsType): Promise => { ensureServerSide(); - if (isPotentialContextFromAppRouter(options) && (await validateContextCookies(options))) { + if (isContextFromNext(options)) { const { req, res, cookies: cookiesFn, ...restOptions } = options; const payload = { name: key, value: stringify(data), ...restOptions }; @@ -116,37 +104,36 @@ const setCookie = async (key: string, data: any, options?: OptionsType): Promise return; } - let _cookieOptions: any; - let _req; - let _res; + let cookieOptions: SerializeOptions = {}; + let req: HttpContext['req']; + let res: HttpContext['res']; if (options) { - const { req, res, ..._options } = options as DefaultOptions; - _req = req; - _res = res; - _cookieOptions = _options; + const { req: _req, res: _res, ...rest } = options as HttpContext; + req = _req; + res = _res; + cookieOptions = rest; } - const cookieStr = serialize(key, stringify(data), { path: '/', ..._cookieOptions }); - if (_res && _req) { - let currentCookies = _res.getHeader('Set-Cookie'); + const cookieStore = serialize(key, stringify(data), { path: '/', ...cookieOptions }); + if (res && req) { + let currentCookies = res.getHeader('Set-Cookie'); if (!Array.isArray(currentCookies)) { currentCookies = !currentCookies ? [] : [String(currentCookies)]; } - _res.setHeader('Set-Cookie', currentCookies.concat(cookieStr)); + res.setHeader('Set-Cookie', currentCookies.concat(cookieStore)); - if (_req && _req.cookies) { - const _cookies = _req.cookies; - data === '' ? delete _cookies[key] : (_cookies[key] = stringify(data)); + if (req && req.cookies) { + const cookies = req.cookies; + data === '' ? delete cookies[key] : (cookies[key] = stringify(data)); } - if (_req && _req.headers && _req.headers.cookie) { - const _cookies = parse(_req.headers.cookie); - - data === '' ? delete _cookies[key] : (_cookies[key] = stringify(data)); + if (req && req.headers && req.headers.cookie) { + const cookies = parse(req.headers.cookie); + data === '' ? delete cookies[key] : (cookies[key] = stringify(data)); - _req.headers.cookie = Object.entries(_cookies).reduce((accum, item) => { + req.headers.cookie = Object.entries(cookies).reduce((accum, item) => { return accum.concat(`${item[0]}=${item[1]};`); }, ''); } diff --git a/src/server/server.test.ts b/src/server/server.test.ts new file mode 100644 index 0000000..9ce92e9 --- /dev/null +++ b/src/server/server.test.ts @@ -0,0 +1,194 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getCookies, getCookie, setCookie, deleteCookie, hasCookie } from './index'; +import { HttpContext, NextCookies } from '../common/types'; +import { IncomingMessage, ServerResponse } from 'http'; + +// Mock Next.js server components +// jest.mock('next/server', () => ({ +// NextRequest: jest.fn(), +// NextResponse: jest.fn(), +// })); + +describe('Server-side cookie functions', () => { + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + // Mock isClientSide to always return false for server-side tests + jest.spyOn(require('../common/utils'), 'isClientSide').mockReturnValue(false); + }); + + describe('getCookies', () => { + describe('Next.js', () => { + it('should return cookies from req.cookies', async () => { + const mockReq = { + cookies: { + set: jest.fn(), + getAll: jest.fn().mockReturnValue([{ name: 'test', value: 'value' }]), + }, + } as unknown as NextRequest; + + const cookies = await getCookies({ req: mockReq }); + expect(cookies).toEqual({ test: 'value' }); + }); + + it('should return cookies from res.cookies', async () => { + const mockRes = { + cookies: { + set: jest.fn(), + getAll: jest.fn().mockReturnValue([{ name: 'test', value: 'value' }]), + }, + } as unknown as NextResponse; + + const cookies = await getCookies({ res: mockRes }); + expect(cookies).toEqual({ test: 'value' }); + }); + + it('should return cookies from cookies()', async () => { + const mockCookies = jest.fn().mockResolvedValue({ + getAll: jest.fn().mockReturnValue([{ name: 'test', value: 'value' }]), + set: jest.fn(), + }); + const cookies = await getCookies({ cookies: mockCookies }); + expect(cookies).toEqual({ test: 'value' }); + }); + }); + + describe('Http', () => { + it('should parse cookies from req.headers.cookie', async () => { + const mockReq = { headers: { cookie: 'test=value' } } as unknown as IncomingMessage; + const cookies = await getCookies({ req: mockReq }); + expect(cookies).toEqual({ test: 'value' }); + }); + + it('should return cookies from req.cookies', async () => { + const mockReq = { cookies: { test: 'value' } } as unknown as IncomingMessage; + const cookies = await getCookies({ req: mockReq }); + expect(cookies).toEqual({ test: 'value' }); + }); + + it('should return empty object if no cookies are present', async () => { + const cookies = await getCookies(); + expect(cookies).toEqual({}); + }); + }); + }); + + describe('getCookie', () => { + it('should return a specific cookie value', async () => { + const mockReq = { + cookies: { + set: jest.fn(), + getAll: jest.fn().mockReturnValue([ + { name: 'test', value: 'value' }, + { name: 'test2', value: 'value2' }, + ]), + }, + } as unknown as NextRequest; + const value = await getCookie('test2', { req: mockReq }); + expect(value).toBe('value2'); + }); + + it('should return undefined for non-existent cookie', async () => { + const mockReq = { + cookies: { + set: jest.fn(), + getAll: jest.fn().mockReturnValue([]), + }, + } as unknown as NextRequest; + const value = await getCookie('nonexistent', { req: mockReq }); + expect(value).toBeUndefined(); + }); + }); + + describe('setCookie', () => { + describe('Next.js', () => { + it('should set a cookie on req', async () => { + const mockReq = { + cookies: { + set: jest.fn(), + getAll: jest.fn().mockReturnValue([]), + }, + } as unknown as NextRequest; + await setCookie('test', 'value', { req: mockReq }); + expect(mockReq.cookies.set).toHaveBeenCalledWith({ name: 'test', value: 'value' }); + }); + + it('should set a cookie on res', async () => { + const mockRes = { + cookies: { + set: jest.fn(), + getAll: jest.fn().mockReturnValue([]), + }, + } as unknown as NextResponse; + await setCookie('test', 'value', { res: mockRes }); + expect(mockRes.cookies.set).toHaveBeenCalledWith({ name: 'test', value: 'value' }); + }); + + it('should set a cookie on cookies()', async () => { + const mockSet = jest.fn(); + const mockCookies = jest.fn().mockResolvedValue({ + set: mockSet, + getAll: jest.fn().mockReturnValue([]), + }); + await setCookie('test', 'value', { cookies: mockCookies }); + expect(mockSet).toHaveBeenCalledWith({ name: 'test', value: 'value' }); + }); + }); + + describe('Http', () => { + it('should set a cookie on headers', async () => { + const mockReq = { headers: { cookie: 'other=cookie;' } } as unknown as IncomingMessage; + const mockRes = { setHeader: jest.fn(), getHeader: jest.fn().mockReturnValue([]) } as unknown as ServerResponse; + await setCookie('test', 'value', { req: mockReq, res: mockRes }); + + expect(mockRes.setHeader).toHaveBeenCalledWith( + 'Set-Cookie', + expect.arrayContaining([expect.stringContaining('test=value;')]), + ); + expect(mockReq.headers.cookie).toBe('other=cookie;test=value;'); + }); + + it('should set a cookie on cookies', async () => { + const mockReq = { cookies: { other: 'cookie' } } as unknown as HttpContext['req']; + const mockRes = { setHeader: jest.fn(), getHeader: jest.fn().mockReturnValue([]) } as unknown as ServerResponse; + await setCookie('test', 'value', { req: mockReq, res: mockRes }); + expect(mockReq?.cookies).toEqual({ other: 'cookie', test: 'value' }); + }); + }); + }); + + describe('deleteCookie', () => { + it('should delete a cookie', async () => { + const mockReq = { cookies: {} } as unknown as HttpContext['req']; + const mockRes = { setHeader: jest.fn(), getHeader: jest.fn().mockReturnValue([]) } as unknown as ServerResponse; + await setCookie('test', 'value', { req: mockReq, res: mockRes }); + expect(mockReq?.cookies).toEqual({ test: 'value' }); + + await deleteCookie('test', { req: mockReq, res: mockRes }); + expect(mockReq?.cookies).toEqual({}); + }); + }); + + describe('hasCookie', () => { + it('should return true if cookie exists', async () => { + const mockReq = { cookies: { existing: 'cookie'} } as unknown as HttpContext['req']; + const mockRes = { setHeader: jest.fn(), getHeader: jest.fn().mockReturnValue([]) } as unknown as ServerResponse; + const has = await hasCookie('existing', { req: mockReq, res: mockRes }); + expect(has).toBe(true); + }); + + it('should return false if cookie does not exist', async () => { + const mockReq = { cookies: {} } as unknown as HttpContext['req']; + const mockRes = { setHeader: jest.fn(), getHeader: jest.fn().mockReturnValue([]) } as unknown as ServerResponse; + const has = await hasCookie('non-existing-cookie', { req: mockReq, res: mockRes }); + expect(has).toBe(false); + }); + }); + + describe('Error handling', () => { + it('should throw an error when trying to access cookies on client-side', async () => { + jest.spyOn(require('../common/utils'), 'isClientSide').mockReturnValue(true); + await expect(getCookies()).rejects.toThrow('You are trying to access cookies on the client side'); + }); + }); +}); diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 889db9b..0000000 --- a/src/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { SerializeOptions } from 'cookie'; -import { IncomingMessage, ServerResponse } from 'http'; -import type { NextRequest, NextResponse } from 'next/server'; -import type { cookies } from 'next/headers'; - -export type OptionsType = DefaultOptions | AppRouterOptions; -export interface DefaultOptions extends SerializeOptions { - res?: ServerResponse; - req?: IncomingMessage & { - cookies?: TmpCookiesObj; - }; - cookies?: CookiesFn; -} - -export type CookiesFn = () => ReturnType; -export type AppRouterOptions = { - res?: Response | NextResponse; - req?: Request | NextRequest; - cookies?: CookiesFn; -}; -export type AppRouterCookies = NextResponse['cookies'] | NextRequest['cookies']; -export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>; -export type CookieValueTypes = string | undefined; - -export interface SyncFunctions { - getCookies: (options?: OptionsType) => TmpCookiesObj; - getCookie: (key: string, options?: OptionsType) => CookieValueTypes; - setCookie: (key: string, data: any, options?: OptionsType) => void; - deleteCookie: (key: string, options?: OptionsType) => void; - hasCookie: (key: string, options?: OptionsType) => boolean; -} From de483ece4a803eb829f55b74061599413e3afcf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 17:59:58 -0300 Subject: [PATCH 13/30] docs: update readme for next.js cookie management usage and remove async-await --- README.md | 113 +++++++++++++++++++++++------------------------------- 1 file changed, 47 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 16df5d4..2be8a7c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A versatile cookie management library for Next.js applications, supporting both ## Installation -For Next.js versions 15 and above, use the latest version of cookies-next. +For Next.js versions 15 and above, use the latest version of cookies-next: ```bash npm install --save cookies-next@latest @@ -30,6 +30,24 @@ npm install --save cookies-next@4.3.0 ### Importing +For Next.js 15+: + +```javascript +// For client-side usage +import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from 'cookies-next/client'; + +// For server-side usage +import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from 'cookies-next/server'; +``` + +Also, you can leave the decision of which import to use to the the library itself, by importing from the root: + +```javascript +import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from 'cookies-next'; +``` + +For Next.js 12.2.0 to 13.x: + ```javascript import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from 'cookies-next'; ``` @@ -39,31 +57,31 @@ import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from 'cooki #### Set a cookie ```javascript -await setCookie('key', 'value', options); +setCookie('key', 'value', options); ``` #### Get a cookie ```javascript -const value = await getCookie('key', options); +const value = getCookie('key', options); ``` #### Get all cookies ```javascript -const cookies = await getCookies(options); +const cookies = getCookies(options); ``` #### Check if a cookie exists ```javascript -const exists = await hasCookie('key', options); +const exists = hasCookie('key', options); ``` #### Delete a cookie ```javascript -await deleteCookie('key', options); +deleteCookie('key', options); ``` ### Client-side Usage @@ -71,30 +89,13 @@ await deleteCookie('key', options); ```javascript 'use client'; -import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next'; +import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next/client'; // Use anywhere in client-side code -await getCookies(); -await getCookie('key'); -await setCookie('key', 'value'); -await deleteCookie('key'); -``` - -### Server-side Usage (Pages Router) - -In `getServerSideProps`: - -```javascript -import { getCookies, getCookie, setCookie, deleteCookie } from 'cookies-next'; - -export const getServerSideProps = async ({ req, res }) => { - await setCookie('test', 'value', { req, res, maxAge: 60 * 60 * 24 }); - await getCookie('test', { req, res }); - await getCookies({ req, res }); - await deleteCookie('test', { req, res }); - - return { props: {} }; -}; +getCookies(); +getCookie('key'); +setCookie('key', 'value'); +deleteCookie('key'); ``` ### Server-side Usage (App Router) @@ -102,7 +103,7 @@ export const getServerSideProps = async ({ req, res }) => { In Server Components: ```javascript -import { getCookie, getCookies, hasCookie } from 'cookies-next'; +import { getCookie, getCookies, hasCookie } from 'cookies-next/server'; import { cookies } from 'next/headers'; export const ServerComponent = async () => { @@ -111,13 +112,9 @@ export const ServerComponent = async () => { const allCookies = await getCookies({ cookies }); const exists = await hasCookie('test', { cookies }); - /** - * Note: It's not possible to update the cookie in RSC. - * - * `setCookie` and `deleteCookie` cannot be used in Server Components - */ - ❌ setCookie("test", "value", { cookies }); // 👉🏻 Won't work. - ❌ deleteCookie('test1', { cookies }); // 👉🏻 Won't work. + // Note: It's not possible to update cookies in Server Components + ❌ setCookie("test", "value", { cookies }); // Won't work + ❌ deleteCookie('test', { cookies }); // Won't work return
...
; }; @@ -129,7 +126,7 @@ In Server Actions: 'use server'; import { cookies } from 'next/headers'; -import { setCookie, deleteCookie, getCookie, getCookies, hasCookie } from 'cookies-next'; +import { setCookie, deleteCookie, getCookie, getCookies, hasCookie } from 'cookies-next/server'; export async function serverAction() { await setCookie('test', 'value', { cookies }); @@ -140,28 +137,12 @@ export async function serverAction() { } ``` -### API Routes (Pages Router) - -```javascript -import { getCookies, getCookie, setCookie, deleteCookie, hasCookie } from 'cookies-next'; - -export default async function handler(req, res) { - await setCookie('key', 'value', { req, res, maxAge: 60 * 60 * 24 }); - await getCookie('key', { req, res }); - await getCookies({ req, res }); - await deleteCookie('key', { req, res }); - await hasCookie('key', { req, res }); - - return res.status(200).json({ message: 'ok' }); -} -``` - ### API Routes (App Router) ```javascript import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; -import { deleteCookie, getCookie, setCookie, hasCookie, getCookies } from 'cookies-next'; +import { deleteCookie, getCookie, setCookie, hasCookie, getCookies } from 'cookies-next/server'; export async function GET(req: NextRequest) { const res = new NextResponse(); @@ -187,7 +168,7 @@ export async function GET(req: NextRequest) { ```javascript import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; -import { getCookie, setCookie, deleteCookie, hasCookie, getCookies } from 'cookies-next'; +import { getCookie, setCookie, deleteCookie, hasCookie, getCookies } from 'cookies-next/server'; export async function middleware(req: NextRequest) { const res = NextResponse.next(); @@ -198,7 +179,7 @@ export async function middleware(req: NextRequest) { await getCookies({ res, req }); // Note: cookies function from next/headers cannot be used in middleware - ❌ setCookie('test', 'value', { cookies }); // 👉🏻 Won't work. + ❌ setCookie('test', 'value', { cookies }); // Won't work return res; } @@ -208,42 +189,42 @@ export async function middleware(req: NextRequest) { ### setCookie(key, value, options) -Sets a cookie. Returns a Promise. +Sets a cookie. ```javascript -await setCookie('key', 'value', options); +setCookie('key', 'value', options); ``` ### getCookie(key, options) -Retrieves a specific cookie. Returns a Promise. +Retrieves a specific cookie. ```javascript -const value = await getCookie('key', options); +const value = getCookie('key', options); ``` ### getCookies(options) -Retrieves all cookies. Returns a Promise. +Retrieves all cookies. ```javascript -const cookies = await getCookies(options); +const cookies = getCookies(options); ``` ### hasCookie(key, options) -Checks if a cookie exists. Returns a Promise. +Checks if a cookie exists. ```javascript -const exists = await hasCookie('key', options); +const exists = hasCookie('key', options); ``` ### deleteCookie(key, options) -Deletes a cookie. Returns a Promise. +Deletes a cookie. ```javascript -await deleteCookie('key', options); +deleteCookie('key', options); ``` ## Options From 594e4566cf6e9668b77ef94890b11360e7ae9b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 18:00:42 -0300 Subject: [PATCH 14/30] feat: add next.js as a peer dependency with version >=15.0.0 in package.json --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 3dc0143..23faa93 100644 --- a/package.json +++ b/package.json @@ -63,5 +63,8 @@ "prettier": "^3.0.2", "ts-jest": "^29.2.5", "typescript": "^4.4.3" + }, + "peerDependencies": { + "next": ">=15.0.0" } } From 1a3b944ce05842b4995c5cda8767e43bb852934e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 18:48:10 -0300 Subject: [PATCH 15/30] chore(release): update version to 5.0.0 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23faa93..4913d90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cookies-next", - "version": "2.1.1", + "version": "5.0.0", "description": "Set, Get, Remove cookies on both client and server side with Next.js", "main": "./lib/index.js", "exports": { From 19b73c9d4e388c82d61bbcceb76337c3a741e632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 18:51:24 -0300 Subject: [PATCH 16/30] chore(package): remove unused @types/cookie dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 4913d90..1052583 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ }, "homepage": "https://github.com/andreizanik/cookies-next#readme", "dependencies": { - "@types/cookie": "^0.6.0", "cookie": "^1.0.1" }, "devDependencies": { From 455f7749184cec7f85a20a6e380ead1a313f7125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Wed, 23 Oct 2024 18:53:33 -0300 Subject: [PATCH 17/30] refactor: remove unused imports and commented mock code in server tests --- src/server/server.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/server/server.test.ts b/src/server/server.test.ts index 9ce92e9..90946ef 100644 --- a/src/server/server.test.ts +++ b/src/server/server.test.ts @@ -1,14 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { getCookies, getCookie, setCookie, deleteCookie, hasCookie } from './index'; -import { HttpContext, NextCookies } from '../common/types'; +import { HttpContext } from '../common/types'; import { IncomingMessage, ServerResponse } from 'http'; -// Mock Next.js server components -// jest.mock('next/server', () => ({ -// NextRequest: jest.fn(), -// NextResponse: jest.fn(), -// })); - describe('Server-side cookie functions', () => { beforeEach(() => { // Reset mocks before each test @@ -171,7 +165,7 @@ describe('Server-side cookie functions', () => { describe('hasCookie', () => { it('should return true if cookie exists', async () => { - const mockReq = { cookies: { existing: 'cookie'} } as unknown as HttpContext['req']; + const mockReq = { cookies: { existing: 'cookie' } } as unknown as HttpContext['req']; const mockRes = { setHeader: jest.fn(), getHeader: jest.fn().mockReturnValue([]) } as unknown as ServerResponse; const has = await hasCookie('existing', { req: mockReq, res: mockRes }); expect(has).toBe(true); From 0bf15828565a9d21e4ddfc123850cd2be1480b55 Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Mon, 28 Oct 2024 22:10:23 +0100 Subject: [PATCH 18/30] refactor: Perform is client side checking options. --- src/common/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/utils.ts b/src/common/utils.ts index 4c7082a..7bc54d8 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,3 +1,5 @@ +import type { CookiesFn, OptionsType } from './types'; + export const stringify = (value: any) => { try { if (typeof value === 'string') { @@ -15,4 +17,6 @@ export const decode = (str: string): string => { return str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); }; -export const isClientSide = () => typeof window !== 'undefined'; +export const isClientSide = (options?: OptionsType) => { + return !options?.req && !options?.res && !(options && 'cookies' in options && (options?.cookies as CookiesFn)); +}; From 9b75eca20b4418852e582ca071bafec99bd4ca6c Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Mon, 28 Oct 2024 22:13:13 +0100 Subject: [PATCH 19/30] refactor: Pass options required for client side check. --- src/client/index.ts | 15 ++++++++------- src/server/index.ts | 16 +++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index ea39e14..b1118f5 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -2,8 +2,8 @@ import { serialize } from 'cookie'; import type { OptionsType, TmpCookiesObj, CookieValueTypes } from '../common/types'; import { stringify, decode, isClientSide } from '../common/utils'; -const ensureClientSide = () => { - if (!isClientSide()) { +const ensureClientSide = (options?: OptionsType) => { + if (!isClientSide(options)) { throw new Error( 'You are trying to access cookies on the server side. Please, use the server-side import with `cookies-next/server` instead.', ); @@ -11,7 +11,7 @@ const ensureClientSide = () => { }; const getCookies = (_options?: OptionsType): TmpCookiesObj => { - ensureClientSide(); + ensureClientSide(_options); const cookies: TmpCookiesObj = {}; const documentCookies = document.cookie ? document.cookie.split('; ') : []; @@ -26,7 +26,8 @@ const getCookies = (_options?: OptionsType): TmpCookiesObj => { }; const getCookie = (key: string, options?: OptionsType): CookieValueTypes => { - ensureClientSide(); + ensureClientSide(options); + const _cookies = getCookies(options); const value = _cookies[key]; if (value === undefined) return undefined; @@ -34,19 +35,19 @@ const getCookie = (key: string, options?: OptionsType): CookieValueTypes => { }; const setCookie = (key: string, data: any, options?: OptionsType): void => { - ensureClientSide(); + ensureClientSide(options); const _cookieOptions = options || {}; const cookieStr = serialize(key, stringify(data), { path: '/', ..._cookieOptions }); document.cookie = cookieStr; }; const deleteCookie = (key: string, options?: OptionsType): void => { - ensureClientSide(); + ensureClientSide(options); setCookie(key, '', { ...options, maxAge: -1 }); }; const hasCookie = (key: string, options?: OptionsType): boolean => { - ensureClientSide(); + ensureClientSide(options); if (!key) return false; const cookies = getCookies(options); return Object.prototype.hasOwnProperty.call(cookies, key); diff --git a/src/server/index.ts b/src/server/index.ts index db1a0e1..ffdc43e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -9,8 +9,8 @@ import type { } from '../common/types'; import { stringify, decode, isClientSide } from '../common/utils'; -const ensureServerSide = () => { - if (isClientSide()) { +const ensureServerSide = (context?: OptionsType) => { + if (isClientSide(context)) { throw new Error( 'You are trying to access cookies on the client side. Please, use the client-side import with `cookies-next/client` instead.', ); @@ -36,7 +36,6 @@ const isContextFromNext = (context?: OptionsType): context is NextContext => { }; const transformAppRouterCookies = (cookies: NextCookies): TmpCookiesObj => { - ensureServerSide(); let _cookies: Partial = {}; cookies.getAll().forEach(({ name, value }) => { _cookies[name] = value; @@ -45,8 +44,7 @@ const transformAppRouterCookies = (cookies: NextCookies): TmpCookiesObj => { }; const getCookies = async (options?: OptionsType): Promise => { - ensureServerSide(); - + ensureServerSide(options); // Use Next.js context if available if (isContextFromNext(options)) { if (options.req) { @@ -78,7 +76,7 @@ const getCookies = async (options?: OptionsType): Promise => { }; const getCookie = async (key: string, options?: OptionsType): Promise => { - ensureServerSide(); + ensureServerSide(options); const cookies = await getCookies(options); const value = cookies[key]; if (value === undefined) return undefined; @@ -86,7 +84,7 @@ const getCookie = async (key: string, options?: OptionsType): Promise => { - ensureServerSide(); + ensureServerSide(options); if (isContextFromNext(options)) { const { req, res, cookies: cookiesFn, ...restOptions } = options; @@ -141,12 +139,12 @@ const setCookie = async (key: string, data: any, options?: OptionsType): Promise }; const deleteCookie = async (key: string, options?: OptionsType): Promise => { - ensureServerSide(); + ensureServerSide(options); return setCookie(key, '', { ...options, maxAge: -1 }); }; const hasCookie = async (key: string, options?: OptionsType): Promise => { - ensureServerSide(); + ensureServerSide(options); if (!key) return false; const cookie = await getCookies(options); return cookie.hasOwnProperty(key); From 6c37bc35e6c2f0513502739ef64915ec53383e98 Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Mon, 28 Oct 2024 22:15:43 +0100 Subject: [PATCH 20/30] refactor: Rebuild smart export to serve functions correctly. --- src/index.ts | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9581d94..76dbcc0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,41 @@ import * as clientCookies from './client'; import * as serverCookies from './server'; export * from './common/types'; +import type { OptionsType } from './common/types'; +import { isClientSide } from './common/utils'; -// Re-export individual functions for backwards compatibility -export const { getCookie, getCookies, setCookie, deleteCookie, hasCookie } = - typeof window === 'undefined' ? serverCookies : clientCookies; +const getRenderPhase = () => (typeof window === 'undefined' ? 'server' : 'client'); + +const getFn = (fnName: keyof typeof clientCookies, options?: OptionsType) => { + if (isClientSide(options)) { + if (getRenderPhase() === 'server') { + // to prevent crash caused by missing window.document during server rendering phase + return; + } + return clientCookies[fnName]; + } + + return serverCookies[fnName]; +}; +export const getCookies = (options?: OptionsType) => { + const fn = getFn('getCookies', options) as ( + options?: OptionsType, + ) => Promise | clientCookies.TmpCookiesObj; + if (!fn) { + return; + } + + return fn(options); +}; +export const getCookie = (key: string, options?: OptionsType) => { + return getFn('getCookie', options)?.(key, options); +}; +export const setCookie = (key: string, data: any, options?: OptionsType) => { + return getFn('setCookie', options)?.(key, data, options); +}; +export const deleteCookie = (key: string, options?: OptionsType) => { + return getFn('setCookie', options)?.(key, options); +}; +export const hasCookie = (key: string, options?: OptionsType) => { + return getFn('hasCookie', options)?.(key, options); +}; From 8f5b29ec3e5bf79d79689902656b562bc67e7854 Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Mon, 28 Oct 2024 22:18:18 +0100 Subject: [PATCH 21/30] fix: Prevent from passing value with incorrect type. --- src/client/client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/client.test.ts b/src/client/client.test.ts index b6f1013..ef82f50 100644 --- a/src/client/client.test.ts +++ b/src/client/client.test.ts @@ -45,6 +45,6 @@ describe('Client-side cookie operations', () => { const complexValue = { key: 'value', nested: { array: [1, 2, 3] } }; setCookie('complexKey', complexValue); const retrievedValue = getCookie('complexKey'); - expect(JSON.parse(retrievedValue as string)).toEqual(complexValue); + expect(typeof retrievedValue === 'string' ? JSON.parse(retrievedValue) : {}).toEqual(complexValue); }); }); From 0ff3dab7a625e0b46c92eae444439288f5dcd2da Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Tue, 29 Oct 2024 22:41:01 +0100 Subject: [PATCH 22/30] fix: Provide correct function name. --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 76dbcc0..7312be7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ export const setCookie = (key: string, data: any, options?: OptionsType) => { return getFn('setCookie', options)?.(key, data, options); }; export const deleteCookie = (key: string, options?: OptionsType) => { - return getFn('setCookie', options)?.(key, options); + return getFn('deleteCookie', options)?.(key, options); }; export const hasCookie = (key: string, options?: OptionsType) => { return getFn('hasCookie', options)?.(key, options); From acd270bd4e71ca459f760c2732debd93dc888f06 Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Tue, 29 Oct 2024 23:03:25 +0100 Subject: [PATCH 23/30] refactor: Redefine interfaces for next context. --- src/common/types.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/common/types.ts b/src/common/types.ts index aad8135..8918264 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,7 +1,26 @@ import { SerializeOptions } from 'cookie'; import { IncomingMessage, ServerResponse } from 'http'; -import type { NextRequest, NextResponse } from 'next/server'; import type { cookies } from 'next/headers'; +import type { RequestCookies, ResponseCookies } from 'next/dist/compiled/@edge-runtime/cookies'; + +/* +We need to declare our own extensions of Request and Response +because NextResponse and NextRequest have an [INTERNALS] property, +which is a symbol that conflicts with the types provided by the user to our exported function. + +The TypeScript error that occurred before this re-declaration was as follows: + + Property '[INTERNALS]' is missing in type 'import("node_modules/next/dist/server/web/spec-extension/response").NextResponse' + but required in type 'import("cookies-next/node_modules/next/dist/server/web/spec-extension/response").NextResponse'.ts(2741) + +*/ +interface NextCookiesRequest extends Request { + get cookies(): RequestCookies; +} + +interface NextCookiesResponse extends Response { + get cookies(): ResponseCookies; +} export interface HttpContext extends SerializeOptions { req?: IncomingMessage & { @@ -10,14 +29,14 @@ export interface HttpContext extends SerializeOptions { }; res?: ServerResponse; } -export interface NextContext { - req?: NextRequest; - res?: NextResponse; +export type NextContext = { + req?: NextCookiesRequest; + res?: NextCookiesResponse; cookies?: CookiesFn; -} +}; export type OptionsType = HttpContext | NextContext; export type CookiesFn = typeof cookies; -export type NextCookies = NextResponse['cookies'] | NextRequest['cookies']; +export type NextCookies = NextCookiesResponse['cookies'] | NextCookiesRequest['cookies']; export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>; export type CookieValueTypes = string | undefined; From 39d2a92efd01bfc33a0b0a066582087d05f69c5b Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Thu, 31 Oct 2024 21:08:58 +0100 Subject: [PATCH 24/30] refactor: Move function for getting render phase to the utils module. --- src/common/utils.ts | 2 ++ src/index.ts | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/utils.ts b/src/common/utils.ts index 7bc54d8..93a0536 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -20,3 +20,5 @@ export const decode = (str: string): string => { export const isClientSide = (options?: OptionsType) => { return !options?.req && !options?.res && !(options && 'cookies' in options && (options?.cookies as CookiesFn)); }; + +export const getRenderPhase = () => (typeof window === 'undefined' ? 'server' : 'client'); diff --git a/src/index.ts b/src/index.ts index 7312be7..4cad7ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,7 @@ import * as clientCookies from './client'; import * as serverCookies from './server'; export * from './common/types'; import type { OptionsType } from './common/types'; -import { isClientSide } from './common/utils'; - -const getRenderPhase = () => (typeof window === 'undefined' ? 'server' : 'client'); +import { getRenderPhase, isClientSide } from './common/utils'; const getFn = (fnName: keyof typeof clientCookies, options?: OptionsType) => { if (isClientSide(options)) { From fad83ba2d091045e33f4a12efb14f01847502f6e Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Thu, 31 Oct 2024 21:09:29 +0100 Subject: [PATCH 25/30] fix: Add missing check for client side functions. --- src/client/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client/index.ts b/src/client/index.ts index b1118f5..47732c5 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,9 +1,13 @@ import { serialize } from 'cookie'; import type { OptionsType, TmpCookiesObj, CookieValueTypes } from '../common/types'; -import { stringify, decode, isClientSide } from '../common/utils'; +import { stringify, decode, isClientSide, getRenderPhase } from '../common/utils'; const ensureClientSide = (options?: OptionsType) => { if (!isClientSide(options)) { + if (getRenderPhase() === 'server') { + // to prevent crash caused by missing window.document during server rendering phase + return; + } throw new Error( 'You are trying to access cookies on the server side. Please, use the server-side import with `cookies-next/server` instead.', ); From 9a5973e902ba18b75dd82e011a1042874eaa2d80 Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel <49715864+greg2012201@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:19:51 +0100 Subject: [PATCH 26/30] chore: Update readme. Co-authored-by: Andrei Zanouski --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2be8a7c..4878920 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ For Next.js versions 15 and above, use the latest version of cookies-next: npm install --save cookies-next@latest ``` -For Next.js versions 12.2.0 to 13.x, use cookies-next version 4.3.0: +For Next.js versions 12.2.0 to 14.x, use cookies-next version 4.3.0: ```bash npm install --save cookies-next@4.3.0 From b860cfde94810137e9b5b0fa7dc226890068848c Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Thu, 31 Oct 2024 21:45:08 +0100 Subject: [PATCH 27/30] fix:Put the server phase guard in the right places. --- src/client/index.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index 47732c5..8b7aa8b 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -4,18 +4,17 @@ import { stringify, decode, isClientSide, getRenderPhase } from '../common/utils const ensureClientSide = (options?: OptionsType) => { if (!isClientSide(options)) { - if (getRenderPhase() === 'server') { - // to prevent crash caused by missing window.document during server rendering phase - return; - } throw new Error( 'You are trying to access cookies on the server side. Please, use the server-side import with `cookies-next/server` instead.', ); } }; -const getCookies = (_options?: OptionsType): TmpCookiesObj => { +const getCookies = (_options?: OptionsType): TmpCookiesObj | undefined => { ensureClientSide(_options); + if (getRenderPhase() === 'server') { + return; + } const cookies: TmpCookiesObj = {}; const documentCookies = document.cookie ? document.cookie.split('; ') : []; @@ -31,15 +30,17 @@ const getCookies = (_options?: OptionsType): TmpCookiesObj => { const getCookie = (key: string, options?: OptionsType): CookieValueTypes => { ensureClientSide(options); - const _cookies = getCookies(options); - const value = _cookies[key]; + const value = _cookies?.[key]; if (value === undefined) return undefined; return decode(value); }; const setCookie = (key: string, data: any, options?: OptionsType): void => { ensureClientSide(options); + if (getRenderPhase() === 'server') { + return; + } const _cookieOptions = options || {}; const cookieStr = serialize(key, stringify(data), { path: '/', ..._cookieOptions }); document.cookie = cookieStr; @@ -54,6 +55,9 @@ const hasCookie = (key: string, options?: OptionsType): boolean => { ensureClientSide(options); if (!key) return false; const cookies = getCookies(options); + if (!cookies) { + return false; + } return Object.prototype.hasOwnProperty.call(cookies, key); }; From b9c28e16da6450216c1e4a7f435a5706cfd143f6 Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Thu, 31 Oct 2024 22:00:51 +0100 Subject: [PATCH 28/30] refactor: Remove redundant guard. --- src/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4cad7ba..0c6c939 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,14 +2,10 @@ import * as clientCookies from './client'; import * as serverCookies from './server'; export * from './common/types'; import type { OptionsType } from './common/types'; -import { getRenderPhase, isClientSide } from './common/utils'; +import { isClientSide } from './common/utils'; const getFn = (fnName: keyof typeof clientCookies, options?: OptionsType) => { if (isClientSide(options)) { - if (getRenderPhase() === 'server') { - // to prevent crash caused by missing window.document during server rendering phase - return; - } return clientCookies[fnName]; } From b9ed5e45e48803f5161682c81c145013f6dfcc51 Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Mon, 4 Nov 2024 21:19:08 +0100 Subject: [PATCH 29/30] fix: Narrow down return types by explicitly returning functions. --- src/index.ts | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0c6c939..cbe9f68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,32 +4,17 @@ export * from './common/types'; import type { OptionsType } from './common/types'; import { isClientSide } from './common/utils'; -const getFn = (fnName: keyof typeof clientCookies, options?: OptionsType) => { - if (isClientSide(options)) { - return clientCookies[fnName]; - } +export const getCookies = (options?: OptionsType) => + isClientSide(options) ? clientCookies.getCookies(options) : serverCookies.getCookies(options); - return serverCookies[fnName]; -}; -export const getCookies = (options?: OptionsType) => { - const fn = getFn('getCookies', options) as ( - options?: OptionsType, - ) => Promise | clientCookies.TmpCookiesObj; - if (!fn) { - return; - } +export const getCookie = (key: string, options?: OptionsType) => + isClientSide(options) ? clientCookies.getCookie(key, options) : serverCookies.getCookie(key, options); - return fn(options); -}; -export const getCookie = (key: string, options?: OptionsType) => { - return getFn('getCookie', options)?.(key, options); -}; -export const setCookie = (key: string, data: any, options?: OptionsType) => { - return getFn('setCookie', options)?.(key, data, options); -}; -export const deleteCookie = (key: string, options?: OptionsType) => { - return getFn('deleteCookie', options)?.(key, options); -}; -export const hasCookie = (key: string, options?: OptionsType) => { - return getFn('hasCookie', options)?.(key, options); -}; +export const setCookie = (key: string, data: any, options?: OptionsType) => + isClientSide(options) ? clientCookies.setCookie(key, data, options) : serverCookies.setCookie(key, data, options); + +export const deleteCookie = (key: string, options?: OptionsType) => + isClientSide(options) ? clientCookies.deleteCookie(key, options) : serverCookies.deleteCookie(key, options); + +export const hasCookie = (key: string, options?: OptionsType) => + isClientSide(options) ? clientCookies.hasCookie(key, options) : serverCookies.hasCookie(key, options); From 38b6c708b02fad26ff3234ad4f6e7e1b35caa48c Mon Sep 17 00:00:00 2001 From: Grzegorz Dubiel Date: Mon, 4 Nov 2024 21:42:35 +0100 Subject: [PATCH 30/30] chore(docs): Explain how to use cookies-next correctly in a client-side component. --- README.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4878920..5a870d9 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,31 @@ deleteCookie('key', options); import { getCookies, setCookie, deleteCookie, getCookie } from 'cookies-next/client'; +function ClientComponent() { + /* + ❗❗❗ In a client component, it's highly recommended to use cookies-next functions within useEffect or in event handlers; otherwise, you might encounter hydration mismatch errors. - + https://react.dev/link/hydration-mismatch. + */ + + useEffect(() => { + getCookies(); + getCookie('key'); + setCookie('key', 'value'); + deleteCookie('key'); + hasCookie('key'); + }, []); + + const handleClick = () => { + getCookies(); + getCookie('key'); + setCookie('key', 'value'); + deleteCookie('key'); + hasCookie('key'); + }; + + /* .... */ +} // Use anywhere in client-side code -getCookies(); -getCookie('key'); -setCookie('key', 'value'); -deleteCookie('key'); ``` ### Server-side Usage (App Router)