From 7a9bbf51a704f483fea5680ca5248200199bb372 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 16 Aug 2023 21:09:29 +0900 Subject: [PATCH 1/2] chore: upgrade edge-runtime/cookies to 3.3.0 - ncc-compiled edge-runtime/cookies only --- packages/next/package.json | 2 +- .../compiled/@edge-runtime/cookies/index.d.ts | 24 ++- .../compiled/@edge-runtime/cookies/index.js | 141 ++++++++++-------- .../@edge-runtime/cookies/package.json | 2 +- 4 files changed, 100 insertions(+), 69 deletions(-) diff --git a/packages/next/package.json b/packages/next/package.json index 6c1e28340d6ba..cf1242ad6e8dc 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -143,7 +143,7 @@ "@babel/traverse": "7.18.0", "@babel/types": "7.18.0", "@capsizecss/metrics": "1.1.0", - "@edge-runtime/cookies": "3.2.1", + "@edge-runtime/cookies": "3.3.0", "@edge-runtime/ponyfill": "2.3.0", "@edge-runtime/primitives": "3.0.1", "@hapi/accept": "5.0.2", diff --git a/packages/next/src/compiled/@edge-runtime/cookies/index.d.ts b/packages/next/src/compiled/@edge-runtime/cookies/index.d.ts index 62ad6ed27ace0..a6b05426e2ef7 100644 --- a/packages/next/src/compiled/@edge-runtime/cookies/index.d.ts +++ b/packages/next/src/compiled/@edge-runtime/cookies/index.d.ts @@ -175,6 +175,7 @@ declare class ResponseCookies { * {@link https://wicg.github.io/cookie-store/#CookieStore-getAll CookieStore#getAll} without the Promise. */ getAll(...args: [key: string] | [options: ResponseCookie] | []): ResponseCookie[]; + has(name: string): boolean; /** * {@link https://wicg.github.io/cookie-store/#CookieStore-set CookieStore#set} without the Promise. */ @@ -182,8 +183,27 @@ declare class ResponseCookies { /** * {@link https://wicg.github.io/cookie-store/#CookieStore-delete CookieStore#delete} without the Promise. */ - delete(...args: [key: string] | [options: ResponseCookie]): this; + delete(...args: [key: string] | [options: Omit]): this; toString(): string; } -export { CookieListItem, RequestCookie, RequestCookies, ResponseCookie, ResponseCookies }; +declare function stringifyCookie(c: ResponseCookie | RequestCookie): string; +/** Parse a `Cookie` header value */ +declare function parseCookie(cookie: string): Map; +/** Parse a `Set-Cookie` header value */ +declare function parseSetCookie(setCookie: string): undefined | ResponseCookie; +/** + * @source https://github.com/nfriedly/set-cookie-parser/blob/master/lib/set-cookie.js + * + * 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 + */ +declare function splitCookiesString(cookiesString: string): string[]; + +export { CookieListItem, RequestCookie, RequestCookies, ResponseCookie, ResponseCookies, parseCookie, parseSetCookie, splitCookiesString, stringifyCookie }; diff --git a/packages/next/src/compiled/@edge-runtime/cookies/index.js b/packages/next/src/compiled/@edge-runtime/cookies/index.js index 9e4fe6dce99ba..97027196fa3ff 100644 --- a/packages/next/src/compiled/@edge-runtime/cookies/index.js +++ b/packages/next/src/compiled/@edge-runtime/cookies/index.js @@ -21,12 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru var src_exports = {}; __export(src_exports, { RequestCookies: () => RequestCookies, - ResponseCookies: () => ResponseCookies + ResponseCookies: () => ResponseCookies, + parseCookie: () => parseCookie, + parseSetCookie: () => parseSetCookie, + splitCookiesString: () => splitCookiesString, + stringifyCookie: () => stringifyCookie }); module.exports = __toCommonJS(src_exports); // src/serialize.ts -function serialize(c) { +function stringifyCookie(c) { var _a; const attrs = [ "path" in c && c.path && `Path=${c.path}`, @@ -39,7 +43,7 @@ function serialize(c) { ].filter(Boolean); return `${c.name}=${encodeURIComponent((_a = c.value) != null ? _a : "")}; ${attrs.join("; ")}`; } -function parseCookieString(cookie) { +function parseCookie(cookie) { const map = /* @__PURE__ */ new Map(); for (const pair of cookie.split(/; */)) { if (!pair) @@ -57,11 +61,11 @@ function parseCookieString(cookie) { } return map; } -function parseSetCookieString(setCookie) { +function parseSetCookie(setCookie) { if (!setCookie) { return void 0; } - const [[name, value], ...attributes] = parseCookieString(setCookie); + const [[name, value], ...attributes] = parseCookie(setCookie); const { domain, expires, httponly, maxage, path, samesite, secure } = Object.fromEntries( attributes.map(([key, value2]) => [key.toLowerCase(), value2]) ); @@ -92,6 +96,57 @@ function parseSameSite(string) { string = string.toLowerCase(); return SAME_SITE.includes(string) ? string : void 0; } +function splitCookiesString(cookiesString) { + if (!cookiesString) + 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 === ",") { + lastComma = pos; + pos += 1; + skipWhitespace(); + nextStart = pos; + while (pos < cookiesString.length && notSpecialChar()) { + pos += 1; + } + if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { + cookiesSeparatorFound = true; + pos = nextStart; + cookiesStrings.push(cookiesString.substring(start, lastComma)); + start = pos; + } else { + pos = lastComma + 1; + } + } else { + pos += 1; + } + } + if (!cookiesSeparatorFound || pos >= cookiesString.length) { + cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); + } + } + return cookiesStrings; +} // src/request-cookies.ts var RequestCookies = class { @@ -101,7 +156,7 @@ var RequestCookies = class { this._headers = requestHeaders; const header = requestHeaders.get("cookie"); if (header) { - const parsed = parseCookieString(header); + const parsed = parseCookie(header); for (const [name, value] of parsed) { this._parsed.set(name, { name, value }); } @@ -138,7 +193,7 @@ var RequestCookies = class { map.set(name, { name, value }); this._headers.set( "cookie", - Array.from(map).map(([_, value2]) => serialize(value2)).join("; ") + Array.from(map).map(([_, value2]) => stringifyCookie(value2)).join("; ") ); return this; } @@ -150,7 +205,7 @@ var RequestCookies = class { const result = !Array.isArray(names) ? map.delete(names) : names.map((name) => map.delete(name)); this._headers.set( "cookie", - Array.from(map).map(([_, value]) => serialize(value)).join("; ") + Array.from(map).map(([_, value]) => stringifyCookie(value)).join("; ") ); return result; } @@ -185,7 +240,7 @@ var ResponseCookies = class { ); const cookieStrings = Array.isArray(setCookie) ? setCookie : splitCookiesString(setCookie); for (const cookieString of cookieStrings) { - const parsed = parseSetCookieString(cookieString); + const parsed = parseSetCookie(cookieString); if (parsed) this._parsed.set(parsed.name, parsed); } @@ -209,6 +264,9 @@ var ResponseCookies = class { const key = typeof args[0] === "string" ? args[0] : (_a = args[0]) == null ? void 0 : _a.name; return all.filter((c) => c.name === key); } + has(name) { + return this._parsed.has(name); + } /** * {@link https://wicg.github.io/cookie-store/#CookieStore-set CookieStore#set} without the Promise. */ @@ -223,20 +281,20 @@ var ResponseCookies = class { * {@link https://wicg.github.io/cookie-store/#CookieStore-delete CookieStore#delete} without the Promise. */ delete(...args) { - const name = typeof args[0] === "string" ? args[0] : args[0].name; - return this.set({ name, value: "", expires: /* @__PURE__ */ new Date(0) }); + const [name, path, domain] = typeof args[0] === "string" ? [args[0]] : [args[0].name, args[0].path, args[0].domain]; + return this.set({ name, path, domain, value: "", expires: /* @__PURE__ */ new Date(0) }); } [Symbol.for("edge-runtime.inspect.custom")]() { return `ResponseCookies ${JSON.stringify(Object.fromEntries(this._parsed))}`; } toString() { - return [...this._parsed.values()].map(serialize).join("; "); + return [...this._parsed.values()].map(stringifyCookie).join("; "); } }; function replace(bag, headers) { headers.delete("set-cookie"); for (const [, value] of bag) { - const serialized = serialize(value); + const serialized = stringifyCookie(value); headers.append("set-cookie", serialized); } } @@ -252,59 +310,12 @@ function normalizeCookie(cookie = { name: "", value: "" }) { } return cookie; } -function splitCookiesString(cookiesString) { - if (!cookiesString) - 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 === ",") { - lastComma = pos; - pos += 1; - skipWhitespace(); - nextStart = pos; - while (pos < cookiesString.length && notSpecialChar()) { - pos += 1; - } - if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { - cookiesSeparatorFound = true; - pos = nextStart; - cookiesStrings.push(cookiesString.substring(start, lastComma)); - start = pos; - } else { - pos = lastComma + 1; - } - } else { - pos += 1; - } - } - if (!cookiesSeparatorFound || pos >= cookiesString.length) { - cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); - } - } - return cookiesStrings; -} // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { RequestCookies, - ResponseCookies + ResponseCookies, + parseCookie, + parseSetCookie, + splitCookiesString, + stringifyCookie }); diff --git a/packages/next/src/compiled/@edge-runtime/cookies/package.json b/packages/next/src/compiled/@edge-runtime/cookies/package.json index d7b3606b9d0e3..ddc6b740d07f9 100644 --- a/packages/next/src/compiled/@edge-runtime/cookies/package.json +++ b/packages/next/src/compiled/@edge-runtime/cookies/package.json @@ -1 +1 @@ -{"name":"@edge-runtime/cookies","version":"3.2.1","main":"./index.js","license":"MPL-2.0"} +{"name":"@edge-runtime/cookies","version":"3.3.0","main":"./index.js","license":"MPL-2.0"} From 563c862d78d725a08341e0560aa541346ab151dd Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 16 Aug 2023 21:10:05 +0900 Subject: [PATCH 2/2] test: add test cases for app-route cookies().has() --- test/e2e/app-dir/app-routes/app-custom-routes.test.ts | 10 ++++++++++ .../app-dir/app-routes/app/hooks/cookies/has/route.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 test/e2e/app-dir/app-routes/app/hooks/cookies/has/route.ts diff --git a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts index 56b627dcb332c..3c8cbf119c139 100644 --- a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts +++ b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts @@ -423,6 +423,16 @@ createNextDescribe( }) }) + describe('cookies().has()', () => { + it('gets the correct values', async () => { + const res = await next.fetch(bathPath + '/hooks/cookies/has') + + expect(res.status).toEqual(200) + + expect(await res.json()).toEqual({ hasCookie: true }) + }) + }) + describe('redirect', () => { it('can respond correctly', async () => { const res = await next.fetch(bathPath + '/hooks/redirect', { diff --git a/test/e2e/app-dir/app-routes/app/hooks/cookies/has/route.ts b/test/e2e/app-dir/app-routes/app/hooks/cookies/has/route.ts new file mode 100644 index 0000000000000..adf2d68e13c38 --- /dev/null +++ b/test/e2e/app-dir/app-routes/app/hooks/cookies/has/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +export async function GET() { + const c = cookies() + c.set('a', 'a') + const hasCookie = c.has('a') + + return NextResponse.json({ hasCookie }) // expect { hasCookie: true } +}