Skip to content

Commit

Permalink
Merge pull request #87 from greg2012201/master
Browse files Browse the repository at this point in the history
feat: add Next.js 15+ support (continuation)
  • Loading branch information
andreizanik authored Nov 8, 2024
2 parents 8dc34e5 + 38b6c70 commit 694d359
Show file tree
Hide file tree
Showing 11 changed files with 733 additions and 491 deletions.
452 changes: 161 additions & 291 deletions README.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jest-environment-jsdom',
};
38 changes: 30 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
{
"name": "cookies-next",
"version": "4.3.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": "5.0.0",
"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"
"pretty": "npx prettier . --write",
"test": "jest"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -35,13 +51,19 @@
},
"homepage": "https://github.com/andreizanik/cookies-next#readme",
"dependencies": {
"cookie": "^0.7.0",
"@types/cookie": "^0.6.0"
"cookie": "^1.0.1"
},
"devDependencies": {
"@types/jest": "^29.5.13",
"@types/node": "^16.10.2",
"next": "^13.4.19",
"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"
},
"peerDependencies": {
"next": ">=15.0.0"
}
}
50 changes: 50 additions & 0 deletions src/client/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getCookie, getCookies, setCookie, deleteCookie, hasCookie } from '../index';

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(typeof retrievedValue === 'string' ? JSON.parse(retrievedValue) : {}).toEqual(complexValue);
});
});
65 changes: 65 additions & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { serialize } from 'cookie';
import type { OptionsType, TmpCookiesObj, CookieValueTypes } from '../common/types';
import { stringify, decode, isClientSide, getRenderPhase } from '../common/utils';

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.',
);
}
};

const getCookies = (_options?: OptionsType): TmpCookiesObj | undefined => {
ensureClientSide(_options);
if (getRenderPhase() === 'server') {
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;
};

const getCookie = (key: string, options?: OptionsType): CookieValueTypes => {
ensureClientSide(options);
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(options);
if (getRenderPhase() === 'server') {
return;
}
const _cookieOptions = options || {};
const cookieStr = serialize(key, stringify(data), { path: '/', ..._cookieOptions });
document.cookie = cookieStr;
};

const deleteCookie = (key: string, options?: OptionsType): void => {
ensureClientSide(options);
setCookie(key, '', { ...options, maxAge: -1 });
};

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);
};

export * from '../common/types';
export { getCookies, getCookie, setCookie, deleteCookie, hasCookie };
42 changes: 42 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SerializeOptions } from 'cookie';
import { IncomingMessage, ServerResponse } from 'http';
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<unknown>'
but required in type 'import("cookies-next/node_modules/next/dist/server/web/spec-extension/response").NextResponse<unknown>'.ts(2741)
*/
interface NextCookiesRequest extends Request {
get cookies(): RequestCookies;
}

interface NextCookiesResponse extends Response {
get cookies(): ResponseCookies;
}

export interface HttpContext extends SerializeOptions {
req?: IncomingMessage & {
// Might be set by third-party libraries such as `cookie-parser`
cookies?: TmpCookiesObj;
};
res?: ServerResponse;
}
export type NextContext = {
req?: NextCookiesRequest;
res?: NextCookiesResponse;
cookies?: CookiesFn;
};
export type OptionsType = HttpContext | NextContext;

export type CookiesFn = typeof cookies;
export type NextCookies = NextCookiesResponse['cookies'] | NextCookiesRequest['cookies'];
export type TmpCookiesObj = { [key: string]: string } | Partial<{ [key: string]: string }>;
export type CookieValueTypes = string | undefined;
24 changes: 24 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { CookiesFn, OptionsType } from './types';

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 = (options?: OptionsType) => {
return !options?.req && !options?.res && !(options && 'cookies' in options && (options?.cookies as CookiesFn));
};

export const getRenderPhase = () => (typeof window === 'undefined' ? 'server' : 'client');
Loading

0 comments on commit 694d359

Please sign in to comment.