├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ └── publish.yml ├── .gitignore ├── .huskyrc.json ├── .vscode └── settings.json ├── .yarn └── releases │ └── yarn-berry.cjs ├── .yarnrc.yml ├── CONTRIBUTING.md ├── README.md ├── examples ├── javascript │ ├── .gitignore │ ├── package.json │ └── pages │ │ ├── create.js │ │ ├── index.js │ │ └── remove.js └── typescript │ ├── .gitignore │ ├── next-env.d.ts │ ├── package.json │ ├── pages │ ├── create.tsx │ ├── index.tsx │ └── remove.tsx │ └── tsconfig.json ├── package.json ├── packages └── nookies │ ├── package.json │ ├── src │ ├── index.ts │ └── utils.ts │ └── tsconfig.json ├── prettier.config.js ├── renovate.json ├── tsconfig.json └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: maticzav 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 45 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 10 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - kind/feature 8 | - kind/bug 9 | - help-wanted 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | # Limit to only `issues` 20 | only: issues 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | - run: yarn 22 | # Release 23 | - name: Copy README.md 24 | run: cp README.md ./packages/nookies/ 25 | - name: Release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | run: | 30 | cd packages/nookies 31 | yarn build 32 | npx semantic-release 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | .DS_Store 5 | *.log* 6 | 7 | packages/**/README.md 8 | 9 | .yarn/cache 10 | .yarn/install-state.gz 11 | .yarn/build-state.yml -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "pretty-quick --staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: '.yarn/releases/yarn-berry.cjs' 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | :wave: 2 | 3 | Thank you for reading this file! We'd love to see you help us make this package even better than it is. Since we are all working on Kayu in our free time, it's important that we follow some set of guidelines to make communication easier. Read this guideline to learn what we have in mind. 4 | 5 | ### Language & Community 6 | 7 | Generally, people are very smart. I am smart, you are smart, everyone here is smart. As a rule of thumb, try to understand what the other person is saying even if it doesn't make any sense. It's highly probable that they _just_ don't know how to explain their (actually) very good idea. 8 | 9 | ### Openning Issues 10 | 11 | Very short; check the docs first, check discussions second, sleep on your problem than open an issue. It's a lot more beneficial if you find a workaroudn and share it than opening up an issue and complaining about it. 12 | 13 | And you'll feel great, I promise. 14 | 15 | ### Requesting Features 16 | 17 | Alright, so you want a new feature, right? Me too! Everyone wants a new feature - a little something that would solve their small (or big) problem. Unfortunatelly, literally _everyone_ wants a feature. 18 | 19 | This is a community driven project. Nobody is paid to work on it. Keep that in mind at all times. 20 | 21 | To save people working on Kayu some time, first check other issues and discussions to see if somebody has requested a similar feature. It's a lot easier for us to prioritise tasks and implement a feature if we see a flourishing discussion in one of the feature requests. Share your thoughts, do research. 22 | 23 | We are inclined to read well structured arguments. If your request hasn't received much attention in say a month, try to do some research on it and share your findings. The more knowledge there already is, the easier (and faster) your feature will be implemented. 24 | 25 | ### Pull Request 26 | 27 | When submitting a pull request, it's important that you correctly label your change. Does it fix something? Does it introduce something new? Breaks something? 28 | 29 | We use semantic versioning. You can read more about it [here](https://semver.org). When creating pull requests, try to follow the semantic versrioning spec or explain how your changes affect the code. Do they break anything? Is this a fix? 30 | 31 | Don't feel discouraged by _all the things you have to learn_. **You can do it.** This is a slowly evolving library, we want to get it right. 32 | 33 | Thank you! You made this library a lot better than it was. :heart: 34 | 35 | ### Adding examples 36 | 37 | Oh cool! We love examples. Create a folder in `examples` that contains your example, and name it `-demo`. Check out existing examples for more guidance! 38 | 39 | ### Development Environment Setup 40 | 41 | You'll need `yarn`. Run `yarn install` to install everything, and you might have to run `yarn build` in `packages/nookies` and `yarn` again to get the examples working. 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nookies :cookie: 2 | 3 | ![Working](https://github.com/maticzav/nookies/workflows/Release/badge.svg) 4 | [![npm version](https://badge.fury.io/js/nookies.svg)](https://badge.fury.io/js/nookies) 5 | 6 | > A collection of cookie helpers for Next.js 7 | 8 | ## Features 9 | 10 | - ✨ SSR support, for setter, parser and destroy 11 | - ⚙️ Custom Express server support 12 | - 🪶 super light 13 | - 🛡 perfect for authentication 14 | 15 | Setting and destroying cookies also works on server-side. 16 | 17 | ## Quick start 18 | 19 | `yarn add nookies` 20 | 21 | > You can play with the example code [here](https://codesandbox.io/s/charming-herschel-7z362). 22 | 23 | ### ServerSide cookies 24 | 25 | ```js 26 | import nookies from 'nookies' 27 | 28 | export default function Me() { 29 | return
My profile
30 | } 31 | 32 | export async function getServerSideProps(ctx) { 33 | // Parse 34 | const cookies = nookies.get(ctx) 35 | 36 | // Set 37 | nookies.set(ctx, 'fromGetInitialProps', 'value', { 38 | maxAge: 30 * 24 * 60 * 60, 39 | path: '/', 40 | }) 41 | 42 | // Destroy 43 | // nookies.destroy(ctx, 'cookieName') 44 | 45 | return { cookies } 46 | } 47 | ``` 48 | 49 | ## Client-only Cookies 50 | 51 | ```js 52 | import { parseCookies, setCookie, destroyCookie } from 'nookies' 53 | 54 | function handleClick() { 55 | // Simply omit context parameter. 56 | // Parse 57 | const cookies = parseCookies() 58 | console.log({ cookies }) 59 | 60 | // Set 61 | setCookie(null, 'fromClient', 'value', { 62 | maxAge: 30 * 24 * 60 * 60, 63 | path: '/', 64 | }) 65 | 66 | // Destroy 67 | // destroyCookie(null, 'cookieName') 68 | } 69 | 70 | export default function Me() { 71 | return 72 | } 73 | ``` 74 | 75 | ## Custom Express server cookies 76 | 77 | ```js 78 | const express = require('express'); 79 | const dev = process.env.NODE_ENV !== 'production'; 80 | const app = next({ dev }); 81 | const handle = app.getRequestHandler(); 82 | const { parseCookies, setCookie, destroyCookie } = require('nookies'); 83 | 84 | app.prepare() 85 | .then(() => { 86 | const server = express(); 87 | 88 | server.get('/page', (req, res) => { 89 | 90 | // Notice how the request object is passed 91 | const parsedCookies = parseCookies({ req }); 92 | 93 | // Notice how the response object is passed 94 | setCookie({ res }, 'fromServer', 'value', { 95 | maxAge: 30 * 24 * 60 * 60, 96 | path: '/page', 97 | }); 98 | 99 | // destroyCookie({ res }, 'fromServer'); 100 | 101 | return handle(req, res); 102 | }); 103 | 104 | ); 105 | ``` 106 | 107 | ## Reference 108 | 109 | > For client side usage, omit the `ctx` parameter. You can do so by setting it to an empty object (`{}`), `null` or `undefined`. 110 | 111 | ### `parseCookies(ctx, options)` or `nookies.get(ctx, options)` 112 | 113 | - **ctx:** `Next.js context` || `(Express request object)` 114 | - **options:** 115 | - **decode:** `a custom resolver function (default: decodeURIComponent)` 116 | 117 | ### `setCookie(ctx, name, value, options)` or `nookies.set(ctx, name, value, options)` 118 | 119 | > Don't forget to end your response on the server with `res.send()`. 120 | 121 | - **ctx:** `(Next.js context)` || `(Express request object)` 122 | - **name:** cookie name 123 | - **value:** cookie value 124 | - **options:** 125 | - **domain** 126 | - **encode** 127 | - **expires** 128 | - **httpOnly** 129 | - **maxAge** 130 | - **path** 131 | - **sameSite** 132 | - **secure** 133 | 134 | ### `destroyCookie(ctx, name, options)` or `nookies.destroy(ctx, 'token', options)` 135 | 136 | > Don't forget to end your response on the server with `res.send()`. This might be the reason your cookie isn't removed. 137 | 138 | - **ctx:** `(Next.js context)` || `(Express response object)` 139 | - **name:** cookie name 140 | - **options:** 141 | - **domain** 142 | - **path** 143 | 144 | ## License 145 | 146 | MIT 147 | -------------------------------------------------------------------------------- /examples/javascript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "10.0.5", 12 | "nookies": "workspace:*", 13 | "react": "17.0.1", 14 | "react-dom": "17.0.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/javascript/pages/create.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import nookies from 'nookies' 4 | 5 | const complex = 6 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) Ap…ML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' 7 | 8 | export default class Create extends React.Component { 9 | static getInitialProps(ctx) { 10 | nookies.set(ctx, 'one', complex, {}) 11 | nookies.set(ctx, 'two', complex, {}) 12 | nookies.set(ctx, 'three', "hey! this one's simple :)", {}) 13 | 14 | return { 15 | server: true, 16 | } 17 | } 18 | 19 | render() { 20 | return ( 21 | <> 22 |

Create

23 | {/* Navigation */} 24 | 39 | 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/javascript/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import nookies from 'nookies' 4 | 5 | export default class Home extends React.Component { 6 | static getInitialProps(ctx) { 7 | let cookies = nookies.get(ctx) 8 | 9 | return { 10 | server: true, 11 | cookies, 12 | } 13 | } 14 | 15 | render() { 16 | const props = this.props 17 | 18 | const server = props && props.server 19 | const cookies = (props && props.cookies) || {} 20 | 21 | return ( 22 | <> 23 |

List

24 | 25 |

26 | Navigate between pages to create, remove and list cookies. You can 27 | also check them in the console. 28 |

29 | {/* Links */} 30 | 45 | 46 | {/* Cookies */} 47 |
48 |

Cookies

49 | 56 |
57 | 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/javascript/pages/remove.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import nookies from 'nookies' 4 | 5 | export default class Remove extends React.Component { 6 | static getInitialProps(ctx) { 7 | const cookies = nookies.get(ctx) 8 | 9 | for (const cookie of Object.keys(cookies)) { 10 | nookies.destroy(ctx, cookie) 11 | } 12 | 13 | return { 14 | server: true, 15 | } 16 | } 17 | 18 | render() { 19 | return ( 20 | <> 21 |

Destroy

22 | 23 | {/* Navigation */} 24 | 39 | 40 | {/* */} 41 | 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/typescript/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "10.0.5", 12 | "nookies": "workspace:*", 13 | "react": "17.0.1", 14 | "react-dom": "17.0.1" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^14.14.21", 18 | "@types/react": "^17.0.0", 19 | "typescript": "^4.1.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/typescript/pages/create.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Link from 'next/link' 4 | import nookies from 'nookies' 5 | 6 | const complex = 7 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) Ap…ML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' 8 | 9 | /* Website, SSR */ 10 | 11 | export default class Create extends React.Component { 12 | static getInitialProps(ctx) { 13 | nookies.set(ctx, 'one', complex, {}) 14 | nookies.set(ctx, 'two', complex, {}) 15 | nookies.set(ctx, 'three', "hey! this one's simple :)", {}) 16 | 17 | return { 18 | server: true, 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 | <> 25 |

Create

26 | {/* Navigation */} 27 | 42 | 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/typescript/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GetServerSideProps } from 'next' 3 | import Link from 'next/link' 4 | import nookies from 'nookies' 5 | 6 | export default function Home(props: any) { 7 | // Data 8 | const cookies = (props && props.cookies) || {} 9 | 10 | return ( 11 | <> 12 |

List

13 | 14 |

15 | Navigate between pages to create, remove and list cookies. You can also 16 | check them in the console. 17 |

18 | {/* Links */} 19 | 34 | 35 | {/* Cookies */} 36 |
37 |

Cookies

38 |
    39 | {Object.keys(cookies).map((name) => ( 40 |
  • 41 | {name} : {cookies[name]} 42 |
  • 43 | ))} 44 |
45 |
46 | 47 | ) 48 | } 49 | 50 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 51 | let cookies = nookies.get(ctx) 52 | 53 | return { 54 | props: { 55 | server: true, 56 | cookies, 57 | }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/typescript/pages/remove.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import nookies from 'nookies' 4 | 5 | /* Website, SSR */ 6 | 7 | export default class Home extends React.Component { 8 | static getInitialProps(ctx) { 9 | const cookies = nookies.get(ctx) 10 | 11 | for (const cookie of Object.keys(cookies)) { 12 | nookies.destroy(ctx, cookie) 13 | } 14 | 15 | return { 16 | server: true, 17 | } 18 | } 19 | 20 | render() { 21 | return ( 22 | <> 23 |

Destroy

24 | 25 |

We've removed every cookie. Check it out in the list page.

26 | 27 | {/* Navigation */} 28 | 43 | 44 | {/* */} 45 | 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "dist", 5 | "target": "es5", 6 | "module": "commonjs", 7 | "lib": ["dom", "es2017"], 8 | "allowJs": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "skipLibCheck": true, 16 | "strict": false, 17 | "moduleResolution": "node" 18 | }, 19 | "include": ["."], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*", 6 | "examples/*" 7 | ], 8 | "scripts": { 9 | "g:tsc": "cd $INIT_CWD && tsc", 10 | "g:prettier": "cd $INIT_CWD && prettier" 11 | }, 12 | "devDependencies": { 13 | "husky": "6.0.0", 14 | "prettier": "2.7.1", 15 | "pretty-quick": "3.1.3", 16 | "rimraf": "3.0.2", 17 | "typescript": "4.7.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/nookies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nookies", 3 | "description": "A set of cookie helpers for Next.js", 4 | "version": "0.0.0-semantic-release", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "maticzav/nookies", 8 | "author": { 9 | "name": "Matic Zavadlal", 10 | "email": "matic.zavadlal@gmail.com" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "yarn g:tsc -d && yarn run minify", 17 | "minify": "terser -c -m --source-map \"content='./dist/index.js.map',url='index.min.js.map'\" -o dist/index.min.js -- dist/index.js" 18 | }, 19 | "dependencies": { 20 | "cookie": "^0.7.0", 21 | "set-cookie-parser": "^2.4.6" 22 | }, 23 | "devDependencies": { 24 | "@types/cookie": "0.5.1", 25 | "@types/express": "4.17.13", 26 | "@types/next": "9.0.0", 27 | "@types/node": "14.18.63", 28 | "@types/set-cookie-parser": "2.4.2", 29 | "terser": "5.14.2" 30 | }, 31 | "keywords": [ 32 | "cookie", 33 | "cookies", 34 | "next", 35 | "nextjs", 36 | "js", 37 | "auth", 38 | "destory", 39 | "parse", 40 | "create", 41 | "nookies" 42 | ], 43 | "release": { 44 | "branch": "master" 45 | }, 46 | "license": "MIT" 47 | } 48 | -------------------------------------------------------------------------------- /packages/nookies/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as cookie from 'cookie' 2 | import * as express from 'express' 3 | import * as next from 'next' 4 | import * as setCookieParser from 'set-cookie-parser' 5 | import { Cookie } from 'set-cookie-parser' 6 | 7 | import { areCookiesEqual, createCookie, isBrowser } from './utils' 8 | 9 | /** 10 | * Parses cookies. 11 | * 12 | * @param ctx NextJS page or API context, express context, null or undefined. 13 | * @param options Options that we pass down to `cookie` library. 14 | */ 15 | export function parseCookies( 16 | ctx?: 17 | | Pick 18 | | { req: next.NextApiRequest } 19 | | { req: express.Request } 20 | | null 21 | | undefined, 22 | options?: cookie.CookieParseOptions, 23 | ) { 24 | if (ctx?.req?.headers?.cookie) { 25 | return cookie.parse(ctx.req.headers.cookie as string, options) 26 | } 27 | 28 | if (isBrowser()) { 29 | return cookie.parse(document.cookie, options) 30 | } 31 | 32 | return {} 33 | } 34 | 35 | /** 36 | * Sets a cookie. 37 | * 38 | * @param ctx NextJS page or API context, express context, null or undefined. 39 | * @param name The name of your cookie. 40 | * @param value The value of your cookie. 41 | * @param options Options that we pass down to `cookie` library. 42 | */ 43 | export function setCookie( 44 | ctx: 45 | | Pick 46 | | { res: next.NextApiResponse } 47 | | { res: express.Response } 48 | | null 49 | | undefined, 50 | name: string, 51 | value: string, 52 | options: cookie.CookieSerializeOptions = {}, 53 | ) { 54 | // SSR 55 | if (ctx?.res?.getHeader && ctx.res.setHeader) { 56 | // Check if response has finished and warn about it. 57 | if (ctx?.res?.finished) { 58 | console.warn(`Not setting "${name}" cookie. Response has finished.`) 59 | console.warn(`You should set cookie before res.send()`) 60 | return {} 61 | } 62 | 63 | /** 64 | * Load existing cookies from the header and parse them. 65 | */ 66 | let cookies = ctx.res.getHeader('Set-Cookie') || [] 67 | 68 | if (typeof cookies === 'string') cookies = [cookies] 69 | if (typeof cookies === 'number') cookies = [] 70 | 71 | /** 72 | * Parse cookies but ignore values - we've already encoded 73 | * them in the previous call. 74 | */ 75 | const parsedCookies = setCookieParser.parse(cookies, { 76 | decodeValues: false, 77 | }) 78 | 79 | /** 80 | * We create the new cookie and make sure that none of 81 | * the existing cookies match it. 82 | */ 83 | const newCookie = createCookie(name, value, options) 84 | let cookiesToSet: string[] = [] 85 | 86 | parsedCookies.forEach((parsedCookie: Cookie) => { 87 | if (!areCookiesEqual(parsedCookie, newCookie)) { 88 | /** 89 | * We serialize the cookie back to the original format 90 | * if it isn't the same as the new one. 91 | */ 92 | const serializedCookie = cookie.serialize( 93 | parsedCookie.name, 94 | parsedCookie.value, 95 | { 96 | // we prevent reencoding by default, but you might override it 97 | encode: (val: string) => val, 98 | ...(parsedCookie as cookie.CookieSerializeOptions), 99 | }, 100 | ) 101 | 102 | cookiesToSet.push(serializedCookie) 103 | } 104 | }) 105 | cookiesToSet.push(cookie.serialize(name, value, options)) 106 | 107 | // Update the header. 108 | ctx.res.setHeader('Set-Cookie', cookiesToSet) 109 | } 110 | 111 | // Browser 112 | if (isBrowser()) { 113 | if (options && options.httpOnly) { 114 | throw new Error('Can not set a httpOnly cookie in the browser.') 115 | } 116 | 117 | document.cookie = cookie.serialize(name, value, options) 118 | } 119 | 120 | return {} 121 | } 122 | 123 | /** 124 | * Destroys a cookie with a particular name. 125 | * 126 | * @param ctx NextJS page or API context, express context, null or undefined. 127 | * @param name Cookie name. 128 | * @param options Options that we pass down to `cookie` library. 129 | */ 130 | export function destroyCookie( 131 | ctx: 132 | | Pick 133 | | { res: next.NextApiResponse } 134 | | { res: express.Response } 135 | | null 136 | | undefined, 137 | name: string, 138 | options?: cookie.CookieSerializeOptions, 139 | ) { 140 | /** 141 | * We forward the request destroy to setCookie function 142 | * as it is the same function with modified maxAge value. 143 | */ 144 | return setCookie(ctx, name, '', { ...(options || {}), maxAge: -1 }) 145 | } 146 | 147 | /* Utility Exports */ 148 | 149 | export default { 150 | set: setCookie, 151 | get: parseCookies, 152 | destroy: destroyCookie, 153 | } 154 | -------------------------------------------------------------------------------- /packages/nookies/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as cookie from 'cookie' 2 | import { Cookie } from 'set-cookie-parser' 3 | 4 | /** 5 | * Tells whether we are in a browser or server. 6 | */ 7 | export function isBrowser(): boolean { 8 | return typeof window !== 'undefined' 9 | } 10 | 11 | export type Dict = { [key: string]: T } 12 | 13 | /** 14 | * Create an instance of the Cookie interface 15 | */ 16 | export function createCookie( 17 | name: string, 18 | value: string, 19 | options: cookie.CookieSerializeOptions, 20 | ): Cookie { 21 | let sameSite = options.sameSite 22 | if (sameSite === true) { 23 | sameSite = 'strict' 24 | } 25 | if (sameSite === undefined || sameSite === false) { 26 | sameSite = 'lax' 27 | } 28 | const cookieToSet = { ...options, sameSite: sameSite } 29 | delete cookieToSet.encode 30 | return { 31 | name: name, 32 | value: value, 33 | ...cookieToSet, 34 | } 35 | } 36 | 37 | /** 38 | * Tells whether given objects have the same properties. 39 | */ 40 | export function hasSameProperties(a: Dict, b: Dict) { 41 | const aProps = Object.getOwnPropertyNames(a) 42 | const bProps = Object.getOwnPropertyNames(b) 43 | 44 | if (aProps.length !== bProps.length) { 45 | return false 46 | } 47 | 48 | for (let i = 0; i < aProps.length; i++) { 49 | const propName = aProps[i] 50 | 51 | if (a[propName] !== b[propName]) { 52 | return false 53 | } 54 | } 55 | 56 | return true 57 | } 58 | 59 | /** 60 | * Compare the cookie and return true if the cookies have equivalent 61 | * options and the cookies would be overwritten in the browser storage. 62 | * 63 | * @param a first Cookie for comparison 64 | * @param b second Cookie for comparison 65 | */ 66 | export function areCookiesEqual(a: Cookie, b: Cookie) { 67 | let sameSiteSame = a.sameSite === b.sameSite 68 | if (typeof a.sameSite === 'string' && typeof b.sameSite === 'string') { 69 | sameSiteSame = a.sameSite.toLowerCase() === b.sameSite.toLowerCase() 70 | } 71 | 72 | return ( 73 | hasSameProperties( 74 | { ...a, sameSite: undefined }, 75 | { ...b, sameSite: undefined }, 76 | ) && sameSiteSame 77 | ) 78 | } 79 | 80 | /* Functions */ 81 | -------------------------------------------------------------------------------- /packages/nookies/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | } 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "docker:disable"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["dom", "es2017"], 6 | 7 | "moduleResolution": "node", 8 | 9 | "noUnusedLocals": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | --------------------------------------------------------------------------------