From df6448ef624b442fa8615574f9ada7a8d3d12e94 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Fri, 12 Dec 2025 16:08:55 -0500 Subject: [PATCH 01/11] convert lib to esm --- .eslintrc.js => .eslintrc.cjs | 5 ++--- lib/set-cookie.js | 13 +++++++------ package.json | 8 ++++++++ test/{.eslintrc.js => .eslintrc.cjs} | 0 test/fetch.js | 5 ++--- test/set-cookie-parser.js | 5 ++--- test/split-cookies-string.js | 7 ++----- test/warnings.js | 5 ++--- 8 files changed, 25 insertions(+), 23 deletions(-) rename .eslintrc.js => .eslintrc.cjs (58%) rename test/{.eslintrc.js => .eslintrc.cjs} (100%) diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 58% rename from .eslintrc.js rename to .eslintrc.cjs index 899c92b..b5666a8 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -1,8 +1,7 @@ "use strict"; module.exports = { - // This isn't really meant for use in browsers, but some dependents such as nookie are. - // So, stick with ES5 to be nice. See #44 - parserOptions: { ecmaVersion: 5 }, + // We tried to stick to ES5 compat for browsers (#44), but eslint can't handle that with modules + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, env: { node: true, browser: true, diff --git a/lib/set-cookie.js b/lib/set-cookie.js index 2d74ca5..a40b180 100644 --- a/lib/set-cookie.js +++ b/lib/set-cookie.js @@ -1,5 +1,3 @@ -"use strict"; - var defaultParseOptions = { decodeValues: true, map: false, @@ -236,7 +234,10 @@ function splitCookiesString(cookiesString) { return cookiesStrings; } -module.exports = parse; -module.exports.parse = parse; -module.exports.parseString = parseString; -module.exports.splitCookiesString = splitCookiesString; +// for backwards compatibility +parse.parse = parse; +parse.parseString = parseString; +parse.splitCookiesString = splitCookiesString; + +export default parse; +export { parse, parseString, splitCookiesString }; diff --git a/package.json b/package.json index e5b6893..d007916 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "lib" ], "main": "./lib/set-cookie.js", + "type": "module", + "exports": { + "module-sync": "./lib/set-cookie.js" + }, "sideEffects": false, "keywords": [ "set-cookie", @@ -23,6 +27,7 @@ "parser" ], "devDependencies": { + "cjstoesm": "^3.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -41,5 +46,8 @@ "license": "MIT", "prettier": { "trailingComma": "es5" + }, + "dependencies": { + "tsc": "^2.0.4" } } diff --git a/test/.eslintrc.js b/test/.eslintrc.cjs similarity index 100% rename from test/.eslintrc.js rename to test/.eslintrc.cjs diff --git a/test/fetch.js b/test/fetch.js index 0f610d0..43961cf 100644 --- a/test/fetch.js +++ b/test/fetch.js @@ -1,6 +1,5 @@ -"use strict"; -var assert = require("assert"); -var setCookie = require("../lib/set-cookie.js"); +import assert from "node:assert"; +import setCookie from "../lib/set-cookie.js"; describe("fetch", () => { before(() => { diff --git a/test/set-cookie-parser.js b/test/set-cookie-parser.js index 1e1e360..308adc0 100644 --- a/test/set-cookie-parser.js +++ b/test/set-cookie-parser.js @@ -1,6 +1,5 @@ -"use strict"; -var assert = require("assert"); -var setCookie = require("../lib/set-cookie.js"); +import assert from "node:assert"; +import setCookie from "../lib/set-cookie.js"; describe("set-cookie-parser", function () { it("should parse a simple set-cookie header", function () { diff --git a/test/split-cookies-string.js b/test/split-cookies-string.js index 1b044d7..73032a4 100644 --- a/test/split-cookies-string.js +++ b/test/split-cookies-string.js @@ -1,8 +1,5 @@ -"use strict"; -var assert = require("assert"); -var setCookie = require("../lib/set-cookie.js"); - -const splitCookiesString = setCookie.splitCookiesString; +import assert from "node:assert"; +import { splitCookiesString } from "../lib/set-cookie.js"; const array = ["a", "b"]; diff --git a/test/warnings.js b/test/warnings.js index e2781d5..b5a72ba 100644 --- a/test/warnings.js +++ b/test/warnings.js @@ -1,6 +1,5 @@ -"use strict"; -var sinon = require("sinon"); -var setCookie = require("../lib/set-cookie.js"); +import sinon from "sinon"; +import setCookie from "../lib/set-cookie.js"; describe("set-cookie-parser", function () { var sandbox = sinon.createSandbox(); From a7136f423481843e1f697bf84b38c11300d90f74 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Wed, 17 Dec 2025 10:55:25 -0500 Subject: [PATCH 02/11] Automatically split conmbined cookies, add split option to override behavior --- lib/set-cookie.js | 20 +++++++++++++--- test/set-cookie-parser.js | 50 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/lib/set-cookie.js b/lib/set-cookie.js index 2d74ca5..454ded4 100644 --- a/lib/set-cookie.js +++ b/lib/set-cookie.js @@ -4,6 +4,7 @@ var defaultParseOptions = { decodeValues: true, map: false, silent: false, + split: undefined, // undefined = split strings but not arrays }; function isForbiddenKey(key) { @@ -129,20 +130,33 @@ function parse(input, options) { input = sch; } } - if (!Array.isArray(input)) { + + var split = options.split; + var isArray = Array.isArray(input); + + if (typeof split === "undefined") { + split = !isArray; + } + + if (!isArray) { input = [input]; } + input = input.filter(isNonEmptyString); + + if (split) { + input = input.map(splitCookiesString).flat(); + } + if (!options.map) { return input - .filter(isNonEmptyString) .map(function (str) { return parseString(str, options); }) .filter(Boolean); } else { var cookies = createNullObj(); - return input.filter(isNonEmptyString).reduce(function (cookies, str) { + return input.reduce(function (cookies, str) { var cookie = parseString(str, options); if (cookie && !isForbiddenKey(cookie.name)) { cookies[cookie.name] = cookie; diff --git a/test/set-cookie-parser.js b/test/set-cookie-parser.js index 1e1e360..98ff273 100644 --- a/test/set-cookie-parser.js +++ b/test/set-cookie-parser.js @@ -247,4 +247,54 @@ describe("set-cookie-parser", function () { expected = {}; assert.deepEqual(actual, expected); }); + + describe("split option", function () { + const cookieA = "a=b"; + const cookieB = `b=c`; + const cookieC = "c=d"; + const combinedCookies = `${cookieA}, ${cookieB}`; + + it("should split when true", function () { + var actual = setCookie.parse(combinedCookies, { split: true }); + var expected = [ + { name: "a", value: "b" }, + { name: "b", value: "c" }, + ]; + assert.deepEqual(actual, expected); + }); + + it("should not split when false", function () { + var actual = setCookie.parse(combinedCookies, { split: false }); + var expected = [{ name: "a", value: "b, b=c" }]; + assert.deepEqual(actual, expected); + }); + + it("should split strings by default", function () { + var actual = setCookie.parse(combinedCookies); + var expected = [ + { name: "a", value: "b" }, + { name: "b", value: "c" }, + ]; + assert.deepEqual(actual, expected); + }); + + it("should not split arrays by default", function () { + var actual = setCookie.parse([combinedCookies, cookieC]); + var expected = [ + { name: "a", value: "b, b=c" }, + { name: "c", value: "d" }, + ]; + assert.deepEqual(actual, expected); + }); + + it("should split arrays when true", function () { + var actual = setCookie.parse([combinedCookies, cookieC], { split: true }); + var expected = [ + { name: "a", value: "b" }, + { name: "b", value: "c" }, + { name: "c", value: "d" }, + ]; + assert.deepEqual(actual, expected); + }); + }); }); From 0f1a2aec4c21ec3b57fc1ae25e655e4b94b00cc3 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Wed, 17 Dec 2025 10:56:53 -0500 Subject: [PATCH 03/11] format command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e5b6893..2542f32 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "lint": "eslint . --ignore-pattern '!.eslintrc.js'", "test": "npm run lint && mocha", "autofix": "npm run lint -- --fix", + "format": "npm run lint -- --fix", "precommit": "npm test" }, "license": "MIT", From be8fbb0f6c952e3ea8a8c9f857f3d0c2a9190e0d Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Wed, 17 Dec 2025 17:29:34 -0500 Subject: [PATCH 04/11] automatically create cjs from mjs --- .eslintrc.cjs | 3 +- .github/workflows/node.js.yml | 3 +- .husky/pre-commit | 1 + build-cjs.js | 16 +++ dist/.eslintrc.cjs | 14 ++ dist/set-cookie.cjs | 258 ++++++++++++++++++++++++++++++++++ lib/set-cookie.js | 2 + package-lock.json | 46 +++--- package.json | 24 ++-- test/cjs.cjs | 24 ++++ test/warnings.js | 2 +- 11 files changed, 357 insertions(+), 36 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 build-cjs.js create mode 100644 dist/.eslintrc.cjs create mode 100644 dist/set-cookie.cjs create mode 100644 test/cjs.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b5666a8..fb3d960 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,6 +1,7 @@ "use strict"; module.exports = { - // We tried to stick to ES5 compat for browsers (#44), but eslint can't handle that with modules + // We tried to stick to ES5 compat for browsers (#44), but eslint can't handle that with modules. + // However, it is enforced for the dist/ directory, which just gets a cjs-ify'd version of the lib. parserOptions: { ecmaVersion: 6, sourceType: 'module' }, env: { node: true, diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index fa1b084..b415868 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -20,8 +20,9 @@ jobs: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm ci - - run: npm run build --if-present - run: npm test + - name: Fail if there are uncommitted changes after building + run: git add . && git diff --quiet && git diff --cached --quiet publish: name: Publish needs: [test] diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..72c4429 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm test diff --git a/build-cjs.js b/build-cjs.js new file mode 100644 index 0000000..ee55f24 --- /dev/null +++ b/build-cjs.js @@ -0,0 +1,16 @@ +// Converts the ESM library file to commonJS for backwards compatibility with older node.js versions and projects +// kind of dumb, but works better than all the "smarter" options I tried + +import { readFileSync, writeFileSync } from "node:fs"; + +const inFile = "lib/set-cookie.js"; +const outFile = "dist/set-cookie.cjs"; + +const header = `// Generated automatically from ${inFile}; see build-cjs.js\n\n`; +const cjsExports = `module.exports = parse;\n`; // the other exports already added as properties on parse in the file + +const input = readFileSync(inFile, { encoding: "utf8" }); +const output = header + input.split("// EXPORTS")[0] + cjsExports; +writeFileSync(outFile, output); + +console.log(`Wrote ${output.length} bytes to ${outFile}`); diff --git a/dist/.eslintrc.cjs b/dist/.eslintrc.cjs new file mode 100644 index 0000000..6c79a94 --- /dev/null +++ b/dist/.eslintrc.cjs @@ -0,0 +1,14 @@ +"use strict"; +module.exports = { + parserOptions: { ecmaVersion: 5 }, + env: { + node: true, + browser: true, + }, + extends: ["eslint:recommended", "plugin:prettier/recommended"], + rules: { + "prefer-const": "error", + strict: "error", + eqeqeq: "error", + }, +}; diff --git a/dist/set-cookie.cjs b/dist/set-cookie.cjs new file mode 100644 index 0000000..3723d36 --- /dev/null +++ b/dist/set-cookie.cjs @@ -0,0 +1,258 @@ +// Generated automatically from lib/set-cookie.js; see build-cjs.js + +var defaultParseOptions = { + decodeValues: true, + map: false, + silent: false, + split: undefined, // undefined = split strings but not arrays +}; + +function isForbiddenKey(key) { + return typeof key !== "string" || key in {}; +} + +function createNullObj() { + return Object.create(null); +} + +function isNonEmptyString(str) { + return typeof str === "string" && !!str.trim(); +} + +function parseString(setCookieValue, options) { + var parts = setCookieValue.split(";").filter(isNonEmptyString); + + var nameValuePairStr = parts.shift(); + var parsed = parseNameValuePair(nameValuePairStr); + var name = parsed.name; + var value = parsed.value; + + options = options + ? Object.assign({}, defaultParseOptions, options) + : defaultParseOptions; + + if (isForbiddenKey(name)) { + return null; + } + + try { + value = options.decodeValues ? decodeURIComponent(value) : value; // decode cookie value + } catch (e) { + console.error( + "set-cookie-parser: failed to decode cookie value. Set options.decodeValues=false to disable decoding.", + e + ); + } + + var cookie = createNullObj(); + cookie.name = name; + cookie.value = value; + + parts.forEach(function (part) { + var sides = part.split("="); + var key = sides.shift().trimLeft().toLowerCase(); + if (isForbiddenKey(key)) { + return; + } + var value = sides.join("="); + if (key === "expires") { + cookie.expires = new Date(value); + } else if (key === "max-age") { + var n = parseInt(value, 10); + if (!Number.isNaN(n)) cookie.maxAge = n; + } else if (key === "secure") { + cookie.secure = true; + } else if (key === "httponly") { + cookie.httpOnly = true; + } else if (key === "samesite") { + cookie.sameSite = value; + } else if (key === "partitioned") { + cookie.partitioned = true; + } else if (key) { + cookie[key] = value; + } + }); + + return cookie; +} + +function parseNameValuePair(nameValuePairStr) { + // Parses name-value-pair according to rfc6265bis draft + + var name = ""; + var value = ""; + var nameValueArr = nameValuePairStr.split("="); + if (nameValueArr.length > 1) { + name = nameValueArr.shift(); + value = nameValueArr.join("="); // everything after the first =, joined by a "=" if there was more than one part + } else { + value = nameValuePairStr; + } + + return { name: name, value: value }; +} + +function parse(input, options) { + options = options + ? Object.assign({}, defaultParseOptions, options) + : defaultParseOptions; + + if (!input) { + if (!options.map) { + return []; + } else { + return createNullObj(); + } + } + + if (input.headers) { + if (typeof input.headers.getSetCookie === "function") { + // for fetch responses - they combine headers of the same type in the headers array, + // but getSetCookie returns an uncombined array + input = input.headers.getSetCookie(); + } else if (input.headers["set-cookie"]) { + // fast-path for node.js (which automatically normalizes header names to lower-case) + input = input.headers["set-cookie"]; + } else { + // slow-path for other environments - see #25 + var sch = + input.headers[ + Object.keys(input.headers).find(function (key) { + return key.toLowerCase() === "set-cookie"; + }) + ]; + // warn if called on a request-like object with a cookie header rather than a set-cookie header - see #34, 36 + if (!sch && input.headers.cookie && !options.silent) { + console.warn( + "Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning." + ); + } + input = sch; + } + } + + var split = options.split; + var isArray = Array.isArray(input); + + if (typeof split === "undefined") { + split = !isArray; + } + + if (!isArray) { + input = [input]; + } + + input = input.filter(isNonEmptyString); + + if (split) { + input = input.map(splitCookiesString).flat(); + } + + if (!options.map) { + return input + .map(function (str) { + return parseString(str, options); + }) + .filter(Boolean); + } else { + var cookies = createNullObj(); + return input.reduce(function (cookies, str) { + var cookie = parseString(str, options); + if (cookie && !isForbiddenKey(cookie.name)) { + cookies[cookie.name] = cookie; + } + return cookies; + }, cookies); + } +} + +/* + Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas + that are within a single set-cookie field-value, such as in the Expires portion. + + This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 + Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 + React Native's fetch does this for *every* header, including set-cookie. + + Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 + Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation +*/ +function splitCookiesString(cookiesString) { + if (Array.isArray(cookiesString)) { + return cookiesString; + } + if (typeof cookiesString !== "string") { + return []; + } + + var cookiesStrings = []; + var pos = 0; + var start; + var ch; + var lastComma; + var nextStart; + var cookiesSeparatorFound; + + function skipWhitespace() { + while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { + pos += 1; + } + return pos < cookiesString.length; + } + + function notSpecialChar() { + ch = cookiesString.charAt(pos); + + return ch !== "=" && ch !== ";" && ch !== ","; + } + + while (pos < cookiesString.length) { + start = pos; + cookiesSeparatorFound = false; + + while (skipWhitespace()) { + ch = cookiesString.charAt(pos); + if (ch === ",") { + // ',' is a cookie separator if we have later first '=', not ';' or ',' + lastComma = pos; + pos += 1; + + skipWhitespace(); + nextStart = pos; + + while (pos < cookiesString.length && notSpecialChar()) { + pos += 1; + } + + // currently special character + if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { + // we found cookies separator + cookiesSeparatorFound = true; + // pos is inside the next cookie, so back up and return it. + pos = nextStart; + cookiesStrings.push(cookiesString.substring(start, lastComma)); + start = pos; + } else { + // in param ',' or param separator ';', + // we continue from that comma + pos = lastComma + 1; + } + } else { + pos += 1; + } + } + + if (!cookiesSeparatorFound || pos >= cookiesString.length) { + cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); + } + } + + return cookiesStrings; +} + +// for backwards compatibility +parse.parse = parse; +parse.parseString = parseString; +parse.splitCookiesString = splitCookiesString; + +module.exports = parse; diff --git a/lib/set-cookie.js b/lib/set-cookie.js index 70023c9..22d2c53 100644 --- a/lib/set-cookie.js +++ b/lib/set-cookie.js @@ -253,5 +253,7 @@ parse.parse = parse; parse.parseString = parseString; parse.splitCookiesString = splitCookiesString; +// EXPORTS +// (this section is replaced by build-cjs.js) export default parse; export { parse, parseString, splitCookiesString }; diff --git a/package-lock.json b/package-lock.json index 25e47b3..c3cc49e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "husky": "^9.0.11", + "husky": "^9.1.7", "mocha": "^10.3.0", "prettier": "^3.2.5", "pretty-quick": "^4.0.0", @@ -305,9 +305,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -410,9 +410,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -999,12 +999,12 @@ } }, "node_modules/husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "bin": { - "husky": "bin.mjs" + "husky": "bin.js" }, "engines": { "node": ">=18" @@ -1382,6 +1382,15 @@ "node": ">= 14.0.0" } }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1435,15 +1444,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -1626,9 +1626,9 @@ } }, "node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true }, "node_modules/picocolors": { diff --git a/package.json b/package.json index cb1fcec..8f4c0c5 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,18 @@ "url": "http://nfriedly.com/" }, "files": [ - "lib" + "lib", + "dist" ], - "main": "./lib/set-cookie.js", + "main": "./dist/set-cookie.cjs", + "module": "./lib/set-cookie.js", "type": "module", "exports": { - "module-sync": "./lib/set-cookie.js" + ".": { + "module-sync": "./lib/set-cookie.js", + "import": "./lib/set-cookie.js", + "require": "./dist/set-cookie.cjs" + } }, "sideEffects": false, "keywords": [ @@ -27,11 +33,10 @@ "parser" ], "devDependencies": { - "cjstoesm": "^3.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "husky": "^9.0.11", + "husky": "^9.1.7", "mocha": "^10.3.0", "prettier": "^3.2.5", "pretty-quick": "^4.0.0", @@ -39,16 +44,15 @@ }, "scripts": { "lint": "eslint . --ignore-pattern '!.eslintrc.js'", - "test": "npm run lint && mocha", + "test": "npm run build && npm run lint && mocha", "autofix": "npm run lint -- --fix", "format": "npm run lint -- --fix", - "precommit": "npm test" + "precommit": "npm test && git add dist/", + "build": "node ./build-cjs.js", + "prepare": "husky" }, "license": "MIT", "prettier": { "trailingComma": "es5" - }, - "dependencies": { - "tsc": "^2.0.4" } } diff --git a/test/cjs.cjs b/test/cjs.cjs new file mode 100644 index 0000000..0661a00 --- /dev/null +++ b/test/cjs.cjs @@ -0,0 +1,24 @@ +const assert = require("node:assert"); +const setCookie = require("../dist/set-cookie.cjs"); + +describe("commonJS (CJS)", function () { + + it('should export all methods', function() { + assert(typeof setCookie === 'function'); + assert(typeof setCookie.parse === 'function'); + assert(typeof setCookie.parseString === 'function'); + assert(typeof setCookie.splitCookiesString === 'function'); + }); + + it('default export should work', function() { + var actual = setCookie("foo=bar;"); + var expected = [{ name: "foo", value: "bar" }]; + assert.deepEqual(actual, expected); + }); + + it('named export should work', function() { + var actual = setCookie.parse("foo=bar;"); + var expected = [{ name: "foo", value: "bar" }]; + assert.deepEqual(actual, expected); + }); +}); \ No newline at end of file diff --git a/test/warnings.js b/test/warnings.js index b5a72ba..3c9ced0 100644 --- a/test/warnings.js +++ b/test/warnings.js @@ -1,7 +1,7 @@ import sinon from "sinon"; import setCookie from "../lib/set-cookie.js"; -describe("set-cookie-parser", function () { +describe("warnings", function () { var sandbox = sinon.createSandbox(); afterEach(function () { From 53016f6011d97cd6e3fbf5235b9d382b668c6b8b Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Wed, 17 Dec 2025 18:27:31 -0500 Subject: [PATCH 05/11] rename parse to parseSetCookie and document things --- CHANGELOG.md | 18 +++++++++ README.md | 80 +++++++++++---------------------------- build-cjs.js | 2 +- dist/set-cookie.cjs | 12 +++--- lib/set-cookie.js | 18 ++++++--- test/set-cookie-parser.js | 58 ++++++++++++++-------------- 6 files changed, 89 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca94cfd..010deee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change log +## [v3.0.0](https://github.com/nfriedly/set-cookie-parser/tree/v3.0.0) - 2025-12-17 + +### Summary of v3 changes +* Library now supports both ESM and CJS +* Combined cookie headers are now split automatically +* API has been simplified to a single named export: `parseSetCookie` (with other exports for backwards compatibility) + +Changed: +* Library is now written as an ES module, with CJS automatically built for backwards-compatibility +* `parse` function renamed to `parseSetCookie` (with alias for backwards compatibility) + +Added: +* Library now splits combined cookies automatically based on input type. +* The new `split` option overrides this behavior, set to `true` to always split or `false` for the previous behavior of never plitting automatically. + +Removed / Soft-deprecated: +* default export, `parseString()`, and `splitCookieString()` methods are no longer documented, but are still present for backwards compatibility. + ## [v2.7.2](https://github.com/nfriedly/set-cookie-parser/tree/v2.7.2) - 2025-10-27 Fixed: diff --git a/README.md b/README.md index 4430e15..092efb0 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,11 @@ --- -ℹ️ **Note for current users:** I'm considering some changes for the next major version and would appreciate your feedback: https://github.com/nfriedly/set-cookie-parser/discussions/68 - ---- - Parses set-cookie headers into JavaScript objects Accepts a single `set-cookie` header value, an array of `set-cookie` header values, a Node.js response object, or a `fetch()` `Response` object that may have 0 or more `set-cookie` headers. -Also accepts an optional options object. Defaults: - -```js -{ - decodeValues: true, // Calls decodeURIComponent on each value - default: true - map: false, // Return an object instead of an array - default: false - silent: false, // Suppress the warning that is logged when called on a request instead of a response - default: false -} -``` - -Returns either an array of cookie objects or a map of name => cookie object with `{map: true}`. Each cookie object will have, at a minimum `name` and `value` properties, and may have additional properties depending on the set-cookie header: +Returns either an array of cookie objects or a map of name => cookie object with options set `{map: true}`. Each cookie object will have, at a minimum `name` and `value` properties, and may have additional properties depending on the set-cookie header: * `name` - cookie name (string) * `value` - cookie value (string) @@ -36,7 +22,7 @@ Returns either an array of cookie objects or a map of name => cookie object with * `secure` - indicates cookie should only be sent over HTTPs (true or undefined) * `httpOnly` - indicates cookie should *not* be accessible to client-side JavaScript (true or undefined) * `sameSite` - indicates if cookie should be included in cross-site requests ([more info](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)) (string or undefined) - * Note: valid values are `"Strict"`, `"Lax"`, and `"None"`, but set-cookie-parser coppies the value verbatim and does *not* perform any validation. + * Note: valid values are `"Strict"`, `"Lax"`, and `"None"`, but set-cookie-parser copies the value verbatim and does *not* perform any validation. * `partitioned` - indicates cookie should be scoped to the combination of 3rd party domain + top page domain ([more info](https://developer.mozilla.org/en-US/docs/Web/Privacy/Privacy_sandbox/Partitioned_cookies)) (true or undefined) (The output format is loosely based on the input format of https://www.npmjs.com/package/cookie) @@ -53,11 +39,11 @@ $ npm install --save set-cookie-parser ### Get array of cookie objects ```js -var http = require('http'); -var setCookie = require('set-cookie-parser'); +import http from 'node:http'; +import { parseSetCookie } from 'set-cookie-parser'; http.get('http://example.com', function(res) { - var cookies = setCookie.parse(res, { + const cookies = parseSetCookie(res, { decodeValues: true // default: true }); @@ -90,16 +76,16 @@ Example output: ### Get map of cookie objects ```js -var http = require('http'); -var setCookie = require('set-cookie-parser'); +import http from 'node:http'; +import { parseSetCookie } from 'set-cookie-parser'; http.get('http://example.com', function(res) { - var cookies = setCookie.parse(res, { + const cookies = parseSetCookie(res, { decodeValues: true, // default: true map: true // default: false }); - var desiredCookie = cookies['session']; + const desiredCookie = cookies['session']; console.log(desiredCookie); }); ``` @@ -129,12 +115,12 @@ Example output: This library can be used in conjunction with the [cookie](https://www.npmjs.com/package/cookie) library to modify and replace set-cookie headers: ```js -const libCookie = require('cookie'); -const setCookie = require('set-cookie-parser'); +import libCookie from 'cookie'; +import { parseSetCookie } from 'set-cookie-parser'; function modifySetCookie(res){ // parse the set-cookie headers with this library - let cookies = setCookie.parse(res); + const cookies = parseSetCookie(res); // modify the cookies here // ... @@ -148,45 +134,23 @@ function modifySetCookie(res){ See a real-world example of this in [unblocker](https://github.com/nfriedly/node-unblocker/blob/08a89ec27274b46dcd80d0a324a59406f2bdad3d/lib/cookies.js#L67-L85) -## Usage in React Native (and with some other fetch implementations) - -React Native follows the Fetch spec more closely and combines all of the Set-Cookie header values into a single string. -The `splitCookiesString` method reverses this. - -```js -var setCookie = require('set-cookie-parser'); - -var response = fetch(/*...*/); - -// This is mainly for React Native; Node.js does not combine set-cookie headers. -var combinedCookieHeader = response.headers.get('Set-Cookie'); -var splitCookieHeaders = setCookie.splitCookiesString(combinedCookieHeader) -var cookies = setCookie.parse(splitCookieHeaders); - -console.log(cookies); // should be an array of cookies -``` - -This behavior may become a default part of parse in the next major release, but requires the extra step for now. - -Note that the `fetch()` spec now includes a `getSetCookie()` method that provides un-combined `Set-Cookie` headers. This library will automatically use that method if it is present. - ## API -### parse(input, [options]) +### parseSetCookie(input, [options]) Parses cookies from a string, array of strings, or a http response object. Always returns an array, regardless of input format. (Unless the `map` option is set, in which case it always returns an object.) -### parseString(individualSetCookieHeader, [options]) - -Parses a single set-cookie header value string. Options default is `{decodeValues: true}`. Used under-the-hood by `parse()`. -Returns an object. - -### splitCookiesString(combinedSetCookieHeader) +Also accepts an optional options object. Defaults: -It's uncommon, but the HTTP spec does allow for multiple of the same header to have their values combined (comma-separated) into a single header. -This method splits apart a combined header without choking on commas that appear within a cookie's value (or expiration date). -Returns an array of strings that may be passed to `parse()`. +```js +{ + decodeValues: true, // Calls decodeURIComponent on each value - default: true + map: false, // Return an object instead of an array - default: false + silent: false, // Suppress the warning that is logged when called on a request instead of a response - default: false + split: undefined, // Automatically separate combined cookie headers. Defaults to true for string inputs and false for array inputs. +} +``` ## References diff --git a/build-cjs.js b/build-cjs.js index ee55f24..648da11 100644 --- a/build-cjs.js +++ b/build-cjs.js @@ -7,7 +7,7 @@ const inFile = "lib/set-cookie.js"; const outFile = "dist/set-cookie.cjs"; const header = `// Generated automatically from ${inFile}; see build-cjs.js\n\n`; -const cjsExports = `module.exports = parse;\n`; // the other exports already added as properties on parse in the file +const cjsExports = `module.exports = parseSetCookie;\n`; // the other exports already added as properties on parse in the file const input = readFileSync(inFile, { encoding: "utf8" }); const output = header + input.split("// EXPORTS")[0] + cjsExports; diff --git a/dist/set-cookie.cjs b/dist/set-cookie.cjs index 3723d36..22ed977 100644 --- a/dist/set-cookie.cjs +++ b/dist/set-cookie.cjs @@ -92,7 +92,7 @@ function parseNameValuePair(nameValuePairStr) { return { name: name, value: value }; } -function parse(input, options) { +function parseSetCookie(input, options) { options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions; @@ -250,9 +250,11 @@ function splitCookiesString(cookiesString) { return cookiesStrings; } +// named export for CJS +parseSetCookie.parseSetCookie = parseSetCookie; // for backwards compatibility -parse.parse = parse; -parse.parseString = parseString; -parse.splitCookiesString = splitCookiesString; +parseSetCookie.parse = parseSetCookie; +parseSetCookie.parseString = parseString; +parseSetCookie.splitCookiesString = splitCookiesString; -module.exports = parse; +module.exports = parseSetCookie; diff --git a/lib/set-cookie.js b/lib/set-cookie.js index 22d2c53..5a1bf5c 100644 --- a/lib/set-cookie.js +++ b/lib/set-cookie.js @@ -90,7 +90,7 @@ function parseNameValuePair(nameValuePairStr) { return { name: name, value: value }; } -function parse(input, options) { +function parseSetCookie(input, options) { options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions; @@ -248,12 +248,18 @@ function splitCookiesString(cookiesString) { return cookiesStrings; } +// named export for CJS +parseSetCookie.parseSetCookie = parseSetCookie; // for backwards compatibility -parse.parse = parse; -parse.parseString = parseString; -parse.splitCookiesString = splitCookiesString; +parseSetCookie.parse = parseSetCookie; +parseSetCookie.parseString = parseString; +parseSetCookie.splitCookiesString = splitCookiesString; // EXPORTS // (this section is replaced by build-cjs.js) -export default parse; -export { parse, parseString, splitCookiesString }; + +// named export for ESM +export { parseSetCookie }; +// for backwards compatibility +export default parseSetCookie; +export { parseSetCookie as parse, parseString, splitCookiesString }; diff --git a/test/set-cookie-parser.js b/test/set-cookie-parser.js index a388962..09ad7a2 100644 --- a/test/set-cookie-parser.js +++ b/test/set-cookie-parser.js @@ -1,26 +1,26 @@ import assert from "node:assert"; -import setCookie from "../lib/set-cookie.js"; +import { parseSetCookie } from "../lib/set-cookie.js"; describe("set-cookie-parser", function () { it("should parse a simple set-cookie header", function () { - var actual = setCookie.parse("foo=bar;"); + var actual = parseSetCookie("foo=bar;"); var expected = [{ name: "foo", value: "bar" }]; assert.deepEqual(actual, expected); }); it("should return empty array on falsy input", function () { var cookieStr = ""; - var actual = setCookie.parse(cookieStr); + var actual = parseSetCookie(cookieStr); var expected = []; assert.deepEqual(actual, expected); cookieStr = null; - actual = setCookie.parse(cookieStr); + actual = parseSetCookie(cookieStr); expected = []; assert.deepEqual(actual, expected); cookieStr = undefined; - actual = setCookie.parse(cookieStr); + actual = parseSetCookie(cookieStr); expected = []; assert.deepEqual(actual, expected); }); @@ -28,7 +28,7 @@ describe("set-cookie-parser", function () { it("should parse a complex set-cookie header", function () { var cookieStr = "foo=bar; Max-Age=1000; Domain=.example.com; Path=/; Expires=Tue, 01 Jul 2025 10:01:11 GMT; HttpOnly; Secure; Partitioned"; - var actual = setCookie.parse(cookieStr); + var actual = parseSetCookie(cookieStr); var expected = [ { name: "foo", @@ -48,7 +48,7 @@ describe("set-cookie-parser", function () { it("should parse a weird but valid cookie", function () { var cookieStr = "foo=bar=bar&foo=foo&John=Doe&Doe=John; Max-Age=1000; Domain=.example.com; Path=/; HttpOnly; Secure"; - var actual = setCookie.parse(cookieStr); + var actual = parseSetCookie(cookieStr); var expected = [ { name: "foo", @@ -65,13 +65,13 @@ describe("set-cookie-parser", function () { it("should parse a cookie with percent-encoding in the data", function () { var cookieStr = "foo=asdf%3Basdf%3Dtrue%3Basdf%3Dasdf%3Basdf%3Dtrue%40asdf"; - var actual = setCookie.parse(cookieStr); + var actual = parseSetCookie(cookieStr); var expected = [ { name: "foo", value: "asdf;asdf=true;asdf=asdf;asdf=true@asdf" }, ]; assert.deepEqual(actual, expected); - actual = setCookie.parse(cookieStr, { decodeValues: false }); + actual = parseSetCookie(cookieStr, { decodeValues: false }); expected = [ { name: "foo", @@ -80,7 +80,7 @@ describe("set-cookie-parser", function () { ]; assert.deepEqual(actual, expected); - actual = setCookie.parse(cookieStr, { decodeValues: true }); + actual = parseSetCookie(cookieStr, { decodeValues: true }); expected = [ { name: "foo", value: "asdf;asdf=true;asdf=asdf;asdf=true@asdf" }, ]; @@ -90,7 +90,7 @@ describe("set-cookie-parser", function () { it("should handle the case when value is not UTF-8 encoded", function () { var cookieStr = "foo=R%F3r%EB%80%8DP%FF%3B%2C%23%9A%0CU%8E%A2C8%D7%3C%3C%B0%DF%17%60%F7Y%DB%16%8BQ%D6%1A"; - var actual = setCookie.parse(cookieStr, { decodeValues: true }); + var actual = parseSetCookie(cookieStr, { decodeValues: true }); var expected = [ { name: "foo", @@ -106,7 +106,7 @@ describe("set-cookie-parser", function () { "bam=baz", "foo=bar; Max-Age=1000; Domain=.example.com; Path=/; Expires=Tue, 01 Jul 2025 10:01:11 GMT; HttpOnly; Secure", ]; - var actual = setCookie.parse(cookieStrs); + var actual = parseSetCookie(cookieStrs); var expected = [ { name: "bam", value: "baz" }, { @@ -132,7 +132,7 @@ describe("set-cookie-parser", function () { ], }, }; - var actual = setCookie.parse(mockResponse); + var actual = parseSetCookie(mockResponse); var expected = [ { name: "bam", value: "baz" }, { @@ -159,7 +159,7 @@ describe("set-cookie-parser", function () { ], }, }; - var actual = setCookie.parse(mockResponse); + var actual = parseSetCookie(mockResponse); var expected = [ { name: "bam", value: "baz" }, { @@ -181,7 +181,7 @@ describe("set-cookie-parser", function () { var mockResponse = { headers: {}, }; - var actual = setCookie.parse(mockResponse); + var actual = parseSetCookie(mockResponse); var expected = []; assert.deepEqual(actual, expected); }); @@ -189,7 +189,7 @@ describe("set-cookie-parser", function () { it("should return object of cookies when result option is set to map", function () { var cookieStr = "foo=bar; Max-Age=1000; Domain=.example.com; Path=/; Expires=Tue, 01 Jul 2025 10:01:11 GMT; HttpOnly; Secure"; - var actual = setCookie.parse(cookieStr, { map: true }); + var actual = parseSetCookie(cookieStr, { map: true }); var expected = { foo: { name: "foo", @@ -207,42 +207,42 @@ describe("set-cookie-parser", function () { it("should return empty object on falsy input when result options is set to map", function () { var cookieStr = ""; - var actual = setCookie.parse(cookieStr, { map: true }); + var actual = parseSetCookie(cookieStr, { map: true }); var expected = {}; assert.deepEqual(actual, expected); cookieStr = null; - actual = setCookie.parse(cookieStr, { map: true }); + actual = parseSetCookie(cookieStr, { map: true }); expected = {}; assert.deepEqual(actual, expected); cookieStr = undefined; - actual = setCookie.parse(cookieStr, { map: true }); + actual = parseSetCookie(cookieStr, { map: true }); expected = {}; assert.deepEqual(actual, expected); }); it("should have empty name string, and value is the name-value-pair if the name-value-pair string lacks a = character", function () { - var actual = setCookie.parse("foo;"); + var actual = parseSetCookie("foo;"); var expected = [{ name: "", value: "foo" }]; assert.deepEqual(actual, expected); - actual = setCookie.parse("foo;SameSite=None;Secure"); + actual = parseSetCookie("foo;SameSite=None;Secure"); expected = [{ name: "", value: "foo", sameSite: "None", secure: true }]; assert.deepEqual(actual, expected); }); it("should skip cookies that could pollute the object prototype", function () { - var actual = setCookie.parse("__proto__=test;"); + var actual = parseSetCookie("__proto__=test;"); var expected = []; assert.deepEqual(actual, expected); - actual = setCookie.parse("foo;__proto__=None;Secure"); + actual = parseSetCookie("foo;__proto__=None;Secure"); expected = [{ name: "", value: "foo", secure: true }]; assert.deepEqual(actual, expected); - actual = setCookie.parse("__proto__=test;", { map: true }); + actual = parseSetCookie("__proto__=test;", { map: true }); expected = {}; assert.deepEqual(actual, expected); }); @@ -254,7 +254,7 @@ describe("set-cookie-parser", function () { const combinedCookies = `${cookieA}, ${cookieB}`; it("should split when true", function () { - var actual = setCookie.parse(combinedCookies, { split: true }); + var actual = parseSetCookie(combinedCookies, { split: true }); var expected = [ { name: "a", value: "b" }, { name: "b", value: "c" }, @@ -263,13 +263,13 @@ describe("set-cookie-parser", function () { }); it("should not split when false", function () { - var actual = setCookie.parse(combinedCookies, { split: false }); + var actual = parseSetCookie(combinedCookies, { split: false }); var expected = [{ name: "a", value: "b, b=c" }]; assert.deepEqual(actual, expected); }); it("should split strings by default", function () { - var actual = setCookie.parse(combinedCookies); + var actual = parseSetCookie(combinedCookies); var expected = [ { name: "a", value: "b" }, { name: "b", value: "c" }, @@ -278,7 +278,7 @@ describe("set-cookie-parser", function () { }); it("should not split arrays by default", function () { - var actual = setCookie.parse([combinedCookies, cookieC]); + var actual = parseSetCookie([combinedCookies, cookieC]); var expected = [ { name: "a", value: "b, b=c" }, { name: "c", value: "d" }, @@ -287,7 +287,7 @@ describe("set-cookie-parser", function () { }); it("should split arrays when true", function () { - var actual = setCookie.parse([combinedCookies, cookieC], { split: true }); + var actual = parseSetCookie([combinedCookies, cookieC], { split: true }); var expected = [ { name: "a", value: "b" }, { name: "b", value: "c" }, From d199494440b55fbbf107039d1726095cc6e97062 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Thu, 18 Dec 2025 10:28:24 -0500 Subject: [PATCH 06/11] misc cleanup - change default value of split to 'auto', document both import styles, comments, etc. --- CHANGELOG.md | 2 +- README.md | 9 ++++++--- build-cjs.js | 2 +- dist/.eslintrc.cjs | 2 ++ lib/set-cookie.js | 4 ++-- test/cjs.cjs | 15 ++++++++------- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 010deee..6299b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ Added: * The new `split` option overrides this behavior, set to `true` to always split or `false` for the previous behavior of never plitting automatically. Removed / Soft-deprecated: -* default export, `parseString()`, and `splitCookieString()` methods are no longer documented, but are still present for backwards compatibility. +* default export, `parse()`, `parseString()`, and `splitCookieString()` methods are no longer documented, but are still present for backwards compatibility. ## [v2.7.2](https://github.com/nfriedly/set-cookie-parser/tree/v2.7.2) - 2025-10-27 diff --git a/README.md b/README.md index 092efb0..d4367ff 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,9 @@ $ npm install --save set-cookie-parser ### Get array of cookie objects ```js -import http from 'node:http'; +import * as http from 'node:http'; import { parseSetCookie } from 'set-cookie-parser'; +// or const { parseSetCookie } = require('set-cookie-parser'); http.get('http://example.com', function(res) { const cookies = parseSetCookie(res, { @@ -76,8 +77,9 @@ Example output: ### Get map of cookie objects ```js -import http from 'node:http'; +import * as http from 'node:http'; import { parseSetCookie } from 'set-cookie-parser'; +// or const { parseSetCookie } = require('set-cookie-parser'); http.get('http://example.com', function(res) { const cookies = parseSetCookie(res, { @@ -115,8 +117,9 @@ Example output: This library can be used in conjunction with the [cookie](https://www.npmjs.com/package/cookie) library to modify and replace set-cookie headers: ```js -import libCookie from 'cookie'; +import * as libCookie from 'cookie'; import { parseSetCookie } from 'set-cookie-parser'; +// or const { parseSetCookie } = require('set-cookie-parser'); function modifySetCookie(res){ // parse the set-cookie headers with this library diff --git a/build-cjs.js b/build-cjs.js index 648da11..78e4b53 100644 --- a/build-cjs.js +++ b/build-cjs.js @@ -7,7 +7,7 @@ const inFile = "lib/set-cookie.js"; const outFile = "dist/set-cookie.cjs"; const header = `// Generated automatically from ${inFile}; see build-cjs.js\n\n`; -const cjsExports = `module.exports = parseSetCookie;\n`; // the other exports already added as properties on parse in the file +const cjsExports = `module.exports = parseSetCookie;\n`; // the other exports are properties on parseSetCookie const input = readFileSync(inFile, { encoding: "utf8" }); const output = header + input.split("// EXPORTS")[0] + cjsExports; diff --git a/dist/.eslintrc.cjs b/dist/.eslintrc.cjs index 6c79a94..bf4d23b 100644 --- a/dist/.eslintrc.cjs +++ b/dist/.eslintrc.cjs @@ -1,5 +1,7 @@ "use strict"; module.exports = { + // This isn't really meant for use in browsers, but some dependents such as nookie are. + // So, stick with ES5 (at least for the CJS version) to be nice. See #44 parserOptions: { ecmaVersion: 5 }, env: { node: true, diff --git a/lib/set-cookie.js b/lib/set-cookie.js index 5a1bf5c..2c7b9b0 100644 --- a/lib/set-cookie.js +++ b/lib/set-cookie.js @@ -2,7 +2,7 @@ var defaultParseOptions = { decodeValues: true, map: false, silent: false, - split: undefined, // undefined = split strings but not arrays + split: "auto", // auto = split strings but not arrays }; function isForbiddenKey(key) { @@ -132,7 +132,7 @@ function parseSetCookie(input, options) { var split = options.split; var isArray = Array.isArray(input); - if (typeof split === "undefined") { + if (split === "auto") { split = !isArray; } diff --git a/test/cjs.cjs b/test/cjs.cjs index 0661a00..e15df98 100644 --- a/test/cjs.cjs +++ b/test/cjs.cjs @@ -1,23 +1,24 @@ const assert = require("node:assert"); -const setCookie = require("../dist/set-cookie.cjs"); +const parseSetCookie = require("../dist/set-cookie.cjs"); describe("commonJS (CJS)", function () { it('should export all methods', function() { - assert(typeof setCookie === 'function'); - assert(typeof setCookie.parse === 'function'); - assert(typeof setCookie.parseString === 'function'); - assert(typeof setCookie.splitCookiesString === 'function'); + assert(typeof parseSetCookie === 'function'); + assert(typeof parseSetCookie.parseSetCookie === 'function'); + assert(typeof parseSetCookie.parse === 'function'); + assert(typeof parseSetCookie.parseString === 'function'); + assert(typeof parseSetCookie.splitCookiesString === 'function'); }); it('default export should work', function() { - var actual = setCookie("foo=bar;"); + var actual = parseSetCookie("foo=bar;"); var expected = [{ name: "foo", value: "bar" }]; assert.deepEqual(actual, expected); }); it('named export should work', function() { - var actual = setCookie.parse("foo=bar;"); + var actual = parseSetCookie.parse("foo=bar;"); var expected = [{ name: "foo", value: "bar" }]; assert.deepEqual(actual, expected); }); From fcc001dc09b6e419d49125090ee5133d97393e0c Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Thu, 18 Dec 2025 10:36:40 -0500 Subject: [PATCH 07/11] fix precommit hook --- .husky/pre-commit | 2 +- dist/set-cookie.cjs | 4 ++-- package.json | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 72c4429..f3ac915 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npm test +npm test && git add dist/ && echo 'precommit done' \ No newline at end of file diff --git a/dist/set-cookie.cjs b/dist/set-cookie.cjs index 22ed977..76d386f 100644 --- a/dist/set-cookie.cjs +++ b/dist/set-cookie.cjs @@ -4,7 +4,7 @@ var defaultParseOptions = { decodeValues: true, map: false, silent: false, - split: undefined, // undefined = split strings but not arrays + split: "auto", // auto = split strings but not arrays }; function isForbiddenKey(key) { @@ -134,7 +134,7 @@ function parseSetCookie(input, options) { var split = options.split; var isArray = Array.isArray(input); - if (typeof split === "undefined") { + if (split === "auto") { split = !isArray; } diff --git a/package.json b/package.json index 8f4c0c5..beb1003 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "test": "npm run build && npm run lint && mocha", "autofix": "npm run lint -- --fix", "format": "npm run lint -- --fix", - "precommit": "npm test && git add dist/", "build": "node ./build-cjs.js", "prepare": "husky" }, From b08860b444e21aa6aafb4176353a5a315d39c690 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Thu, 18 Dec 2025 10:43:46 -0500 Subject: [PATCH 08/11] Update options.split default value and clarify use --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d4367ff..d277299 100644 --- a/README.md +++ b/README.md @@ -148,10 +148,10 @@ Also accepts an optional options object. Defaults: ```js { - decodeValues: true, // Calls decodeURIComponent on each value - default: true - map: false, // Return an object instead of an array - default: false - silent: false, // Suppress the warning that is logged when called on a request instead of a response - default: false - split: undefined, // Automatically separate combined cookie headers. Defaults to true for string inputs and false for array inputs. + decodeValues: true, // Calls decodeURIComponent on each value - default: true + map: false, // Return an object instead of an array - default: false + silent: false, // Suppress the warning that is logged when called on a request instead of a response - default: false + split: 'auto', // Separate combined cookie headers. Valid options are true/false/'auto'. 'auto' splits strings but not arrays. } ``` From 473605faf5a3918184d61ff03369ef198c294edb Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Thu, 18 Dec 2025 10:47:24 -0500 Subject: [PATCH 09/11] comment fix --- test/set-cookie-parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/set-cookie-parser.js b/test/set-cookie-parser.js index 09ad7a2..980de6e 100644 --- a/test/set-cookie-parser.js +++ b/test/set-cookie-parser.js @@ -249,7 +249,7 @@ describe("set-cookie-parser", function () { describe("split option", function () { const cookieA = "a=b"; - const cookieB = `b=c`; + const cookieB = "b=c"; const cookieC = "c=d"; const combinedCookies = `${cookieA}, ${cookieB}`; From ba854bb593eac8c19a149d48bc0ae235ea03a43d Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Wed, 7 Jan 2026 10:33:23 -0500 Subject: [PATCH 10/11] remove transpiled CJS from git, drop check for uncommitted changes --- .github/workflows/node.js.yml | 2 - .gitignore | 1 + dist/set-cookie.cjs | 260 ---------------------------------- 3 files changed, 1 insertion(+), 262 deletions(-) delete mode 100644 dist/set-cookie.cjs diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index b415868..ce613c4 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -21,8 +21,6 @@ jobs: cache: 'npm' - run: npm ci - run: npm test - - name: Fail if there are uncommitted changes after building - run: git add . && git diff --quiet && git diff --cached --quiet publish: name: Publish needs: [test] diff --git a/.gitignore b/.gitignore index e61051f..b55927f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ /coverage/ +/dist/set-cookie.cjs \ No newline at end of file diff --git a/dist/set-cookie.cjs b/dist/set-cookie.cjs deleted file mode 100644 index 76d386f..0000000 --- a/dist/set-cookie.cjs +++ /dev/null @@ -1,260 +0,0 @@ -// Generated automatically from lib/set-cookie.js; see build-cjs.js - -var defaultParseOptions = { - decodeValues: true, - map: false, - silent: false, - split: "auto", // auto = split strings but not arrays -}; - -function isForbiddenKey(key) { - return typeof key !== "string" || key in {}; -} - -function createNullObj() { - return Object.create(null); -} - -function isNonEmptyString(str) { - return typeof str === "string" && !!str.trim(); -} - -function parseString(setCookieValue, options) { - var parts = setCookieValue.split(";").filter(isNonEmptyString); - - var nameValuePairStr = parts.shift(); - var parsed = parseNameValuePair(nameValuePairStr); - var name = parsed.name; - var value = parsed.value; - - options = options - ? Object.assign({}, defaultParseOptions, options) - : defaultParseOptions; - - if (isForbiddenKey(name)) { - return null; - } - - try { - value = options.decodeValues ? decodeURIComponent(value) : value; // decode cookie value - } catch (e) { - console.error( - "set-cookie-parser: failed to decode cookie value. Set options.decodeValues=false to disable decoding.", - e - ); - } - - var cookie = createNullObj(); - cookie.name = name; - cookie.value = value; - - parts.forEach(function (part) { - var sides = part.split("="); - var key = sides.shift().trimLeft().toLowerCase(); - if (isForbiddenKey(key)) { - return; - } - var value = sides.join("="); - if (key === "expires") { - cookie.expires = new Date(value); - } else if (key === "max-age") { - var n = parseInt(value, 10); - if (!Number.isNaN(n)) cookie.maxAge = n; - } else if (key === "secure") { - cookie.secure = true; - } else if (key === "httponly") { - cookie.httpOnly = true; - } else if (key === "samesite") { - cookie.sameSite = value; - } else if (key === "partitioned") { - cookie.partitioned = true; - } else if (key) { - cookie[key] = value; - } - }); - - return cookie; -} - -function parseNameValuePair(nameValuePairStr) { - // Parses name-value-pair according to rfc6265bis draft - - var name = ""; - var value = ""; - var nameValueArr = nameValuePairStr.split("="); - if (nameValueArr.length > 1) { - name = nameValueArr.shift(); - value = nameValueArr.join("="); // everything after the first =, joined by a "=" if there was more than one part - } else { - value = nameValuePairStr; - } - - return { name: name, value: value }; -} - -function parseSetCookie(input, options) { - options = options - ? Object.assign({}, defaultParseOptions, options) - : defaultParseOptions; - - if (!input) { - if (!options.map) { - return []; - } else { - return createNullObj(); - } - } - - if (input.headers) { - if (typeof input.headers.getSetCookie === "function") { - // for fetch responses - they combine headers of the same type in the headers array, - // but getSetCookie returns an uncombined array - input = input.headers.getSetCookie(); - } else if (input.headers["set-cookie"]) { - // fast-path for node.js (which automatically normalizes header names to lower-case) - input = input.headers["set-cookie"]; - } else { - // slow-path for other environments - see #25 - var sch = - input.headers[ - Object.keys(input.headers).find(function (key) { - return key.toLowerCase() === "set-cookie"; - }) - ]; - // warn if called on a request-like object with a cookie header rather than a set-cookie header - see #34, 36 - if (!sch && input.headers.cookie && !options.silent) { - console.warn( - "Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning." - ); - } - input = sch; - } - } - - var split = options.split; - var isArray = Array.isArray(input); - - if (split === "auto") { - split = !isArray; - } - - if (!isArray) { - input = [input]; - } - - input = input.filter(isNonEmptyString); - - if (split) { - input = input.map(splitCookiesString).flat(); - } - - if (!options.map) { - return input - .map(function (str) { - return parseString(str, options); - }) - .filter(Boolean); - } else { - var cookies = createNullObj(); - return input.reduce(function (cookies, str) { - var cookie = parseString(str, options); - if (cookie && !isForbiddenKey(cookie.name)) { - cookies[cookie.name] = cookie; - } - return cookies; - }, cookies); - } -} - -/* - Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas - that are within a single set-cookie field-value, such as in the Expires portion. - - This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 - Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 - React Native's fetch does this for *every* header, including set-cookie. - - Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 - Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation -*/ -function splitCookiesString(cookiesString) { - if (Array.isArray(cookiesString)) { - return cookiesString; - } - if (typeof cookiesString !== "string") { - return []; - } - - var cookiesStrings = []; - var pos = 0; - var start; - var ch; - var lastComma; - var nextStart; - var cookiesSeparatorFound; - - function skipWhitespace() { - while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { - pos += 1; - } - return pos < cookiesString.length; - } - - function notSpecialChar() { - ch = cookiesString.charAt(pos); - - return ch !== "=" && ch !== ";" && ch !== ","; - } - - while (pos < cookiesString.length) { - start = pos; - cookiesSeparatorFound = false; - - while (skipWhitespace()) { - ch = cookiesString.charAt(pos); - if (ch === ",") { - // ',' is a cookie separator if we have later first '=', not ';' or ',' - lastComma = pos; - pos += 1; - - skipWhitespace(); - nextStart = pos; - - while (pos < cookiesString.length && notSpecialChar()) { - pos += 1; - } - - // currently special character - if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { - // we found cookies separator - cookiesSeparatorFound = true; - // pos is inside the next cookie, so back up and return it. - pos = nextStart; - cookiesStrings.push(cookiesString.substring(start, lastComma)); - start = pos; - } else { - // in param ',' or param separator ';', - // we continue from that comma - pos = lastComma + 1; - } - } else { - pos += 1; - } - } - - if (!cookiesSeparatorFound || pos >= cookiesString.length) { - cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); - } - } - - return cookiesStrings; -} - -// named export for CJS -parseSetCookie.parseSetCookie = parseSetCookie; -// for backwards compatibility -parseSetCookie.parse = parseSetCookie; -parseSetCookie.parseString = parseString; -parseSetCookie.splitCookiesString = splitCookiesString; - -module.exports = parseSetCookie; From 16b13bb136bd3e21ddb3e4a93d57dfa5a9fa5323 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Wed, 7 Jan 2026 10:34:37 -0500 Subject: [PATCH 11/11] simplify pre-commit hook --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index f3ac915..18de984 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npm test && git add dist/ && echo 'precommit done' \ No newline at end of file +npm test \ No newline at end of file