From e57abfee52685f6b00425904c68c84da84b52b53 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 21 Sep 2023 15:17:48 +0900 Subject: [PATCH] feat: add `setCookieLocale` --- bun.lockb | Bin 129719 -> 140023 bytes package.json | 2 + src/h3.test.ts | 81 ++++++++++++++++++++++++++++-- src/h3.ts | 40 ++++++++++++++- src/http.ts | 126 +++++++++++++++++++++++++++++++++++++++++++++++ src/node.test.ts | 59 +++++++++++++++++++++- src/node.ts | 52 +++++++++++++++++-- src/web.test.ts | 40 ++++++++++++++- src/web.ts | 54 +++++++++++++++++++- 9 files changed, 441 insertions(+), 13 deletions(-) diff --git a/bun.lockb b/bun.lockb index 9c7d85d5c1d931a237ebe1ac8a268beec436e9fd..0cf56fd5fd11f771292bafb65d95f38aade1b8b0 100755 GIT binary patch delta 29092 zcmeHw2Ut``*Z-Y`RaZq(RJw?Woh~d$S+OfBR

=OHn#lKoQVrjNOcSjZtIoC2B0# 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 delta 23120 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 b(RKFnhRnK_FKXg!mUesw&OI~H#83Y(II^k& diff --git a/package.json b/package.json index d42c720..edcdc71 100644 --- a/package.json +++ b/package.json @@ -79,12 +79,14 @@ }, "devDependencies": { "@types/node": "^20.6.0", + "@types/supertest": "^2.0.12", "@vitest/coverage-v8": "^0.34.4", "bumpp": "^9.2.0", "cookie-es": "^1.0.0", "gh-changelogen": "^0.2.8", "h3": "^1.8.1", "lint-staged": "^14.0.0", + "supertest": "^6.3.3", "typescript": "^5.2.2", "unbuild": "^2.0.0", "vitest": "^0.34.4" diff --git a/src/h3.test.ts b/src/h3.test.ts index 99f07d5..495c981 100644 --- a/src/h3.test.ts +++ b/src/h3.test.ts @@ -1,8 +1,16 @@ -import { describe, expect, test } from 'vitest' -import { getAcceptLanguages, getCookieLocale, getLocale } from './h3.ts' +import { beforeEach, describe, expect, test } from 'vitest' +import { createApp, eventHandler, toNodeListener } from 'h3' +import supertest from 'supertest' +import { + getAcceptLanguages, + getCookieLocale, + getLocale, + setCookieLocale, +} from './h3.ts' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' -import type { H3Event } from 'h3' +import type { App, H3Event } from 'h3' +import type { SuperTest, Test } from 'supertest' describe('getAcceptLanguages', () => { test('basic', () => { @@ -192,3 +200,70 @@ describe('getCookieLocale', () => { .toThrowError(RangeError) }) }) + +describe('setCookieLocale', () => { + let app: App + let request: SuperTest + + beforeEach(() => { + app = createApp({ debug: false }) + request = supertest(toNodeListener(app)) + }) + + test('specify Locale instance', async () => { + app.use( + '/', + eventHandler((event) => { + const locale = new Intl.Locale('ja-JP') + setCookieLocale(event, locale) + return '200' + }), + ) + const result = await request.get('/') + expect(result.headers['set-cookie']).toEqual([ + 'i18n_locale=ja-JP; Path=/', + ]) + }) + + test('specify language tag', async () => { + app.use( + '/', + eventHandler((event) => { + setCookieLocale(event, 'ja-JP') + return '200' + }), + ) + const result = await request.get('/') + expect(result.headers['set-cookie']).toEqual([ + 'i18n_locale=ja-JP; Path=/', + ]) + }) + + test('specify cookie name', async () => { + app.use( + '/', + eventHandler((event) => { + setCookieLocale(event, 'ja-JP', { name: 'intlify_locale' }) + return '200' + }), + ) + const result = await request.get('/') + expect(result.headers['set-cookie']).toEqual([ + 'intlify_locale=ja-JP; Path=/', + ]) + }) + + test('Syntax Error', () => { + const eventMock = { + node: { + req: { + method: 'GET', + headers: {}, + }, + }, + } as H3Event + + expect(() => setCookieLocale(eventMock, 'j')) + .toThrowError(/locale is invalid: j/) + }) +}) diff --git a/src/h3.ts b/src/h3.ts index 0a17d57..afc77eb 100644 --- a/src/h3.ts +++ b/src/h3.ts @@ -1,8 +1,13 @@ -import { getAcceptLanguagesWithGetter, getLocaleWithGetter } from './http.ts' -import { getCookie, getHeaders } from 'h3' +import { + getAcceptLanguagesWithGetter, + getLocaleWithGetter, + validateLocale, +} from './http.ts' +import { getCookie, getHeaders, setCookie } from 'h3' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' import type { H3Event } from 'h3' +import type { CookieOptions } from './http.ts' /** * get accpet languages @@ -97,3 +102,34 @@ export function getCookieLocale( ): Intl.Locale { return getLocaleWithGetter(() => getCookie(event, name) || lang) } + +/** + * set locale to the response `Set-Cookie` header. + * + * @example + * example for h3: + * + * ```ts + * import { createApp, eventHandler } from 'h3' + * import { getCookieLocale } from '@intlify/utils/h3' + * + * app.use(eventHandler(event) => { + * setCookieLocale(event, 'ja-JP') + * // ... + * }) + * ``` + * + * @param {H3Event} event The {@link H3Event | H3} event + * @param {string | Intl.Locale} locale The locale value + * @param {CookieOptions} options The cookie options, `name` option is `i18n_locale` as default, and `path` option is `/` as default. + * + * @throws {SyntaxError} Throws the {@link SyntaxError} if `locale` is invalid. + */ +export function setCookieLocale( + event: H3Event, + locale: string | Intl.Locale, + options: CookieOptions = { name: DEFAULT_COOKIE_NAME, path: '/' }, +): void { + validateLocale(locale) + setCookie(event, options.name!, locale.toString(), options) +} diff --git a/src/http.ts b/src/http.ts index 48ae9ed..0a9e742 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,4 +1,107 @@ import { parseAcceptLanguage } from './shared.ts' +import { isLocale, validateLanguageTag } from './shared.ts' + +// import type { CookieSerializeOptions } from 'cookie-es' +// NOTE: This is a copy of the type definition from `cookie-es` package, we want to avoid building error for this type definition ... + +interface CookieSerializeOptions { + /** + * Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no + * domain is set, and most clients will consider the cookie to apply to only + * the current domain. + */ + domain?: string | undefined + /** + * Specifies a function that will be used to encode a cookie's value. Since + * value of a cookie has a limited character set (and must be a simple + * string), this function can be used to encode a value into a string suited + * for a cookie's value. + * + * The default function is the global `encodeURIComponent`, which will + * encode a JavaScript string into UTF-8 byte sequences and then URL-encode + * any that fall outside of the cookie range. + */ + encode?(value: string): string + /** + * Specifies the `Date` object to be the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.1|`Expires` `Set-Cookie` attribute}. By default, + * no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete + * it on a condition like exiting a web browser application. + * + * *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification} + * states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is + * possible not all clients by obey this, so if both are set, they should + * point to the same date and time. + */ + expires?: Date | undefined + /** + * Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.6|`HttpOnly` `Set-Cookie` attribute}. + * When truthy, the `HttpOnly` attribute is set, otherwise it is not. By + * default, the `HttpOnly` attribute is not set. + * + * *Note* be careful when setting this to true, as compliant clients will + * not allow client-side JavaScript to see the cookie in `document.cookie`. + */ + httpOnly?: boolean | undefined + /** + * Specifies the number (in seconds) to be the value for the `Max-Age` + * `Set-Cookie` attribute. The given number will be converted to an integer + * by rounding down. By default, no maximum age is set. + * + * *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification} + * states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is + * possible not all clients by obey this, so if both are set, they should + * point to the same date and time. + */ + maxAge?: number | undefined + /** + * Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.4|`Path` `Set-Cookie` attribute}. + * By default, the path is considered the "default path". + */ + path?: string | undefined + /** + * Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1]. + * + * - `'low'` will set the `Priority` attribute to `Low`. + * - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set. + * - `'high'` will set the `Priority` attribute to `High`. + * + * More information about the different priority levels can be found in + * [the specification][rfc-west-cookie-priority-00-4.1]. + * + * **note** This is an attribute that has not yet been fully standardized, and may change in the future. + * This also means many clients may ignore this attribute until they understand it. + */ + priority?: 'low' | 'medium' | 'high' | undefined + /** + * Specifies the boolean or string to be the value for the {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|`SameSite` `Set-Cookie` attribute}. + * + * - `true` will set the `SameSite` attribute to `Strict` for strict same + * site enforcement. + * - `false` will not set the `SameSite` attribute. + * - `'lax'` will set the `SameSite` attribute to Lax for lax same site + * enforcement. + * - `'strict'` will set the `SameSite` attribute to Strict for strict same + * site enforcement. + * - `'none'` will set the SameSite attribute to None for an explicit + * cross-site cookie. + * + * More information about the different enforcement levels can be found in {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|the specification}. + * + * *note* This is an attribute that has not yet been fully standardized, and may change in the future. This also means many clients may ignore this attribute until they understand it. + */ + sameSite?: true | false | 'lax' | 'strict' | 'none' | undefined + /** + * Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.5|`Secure` `Set-Cookie` attribute}. When truthy, the + * `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. + * + * *Note* be careful when setting this to `true`, as compliant clients will + * not send the cookie back to the server in the future if the browser does + * not have an HTTPS connection. + */ + secure?: boolean | undefined +} + +export type CookieOptions = CookieSerializeOptions & { name?: string } export function getAcceptLanguagesWithGetter( getter: () => string | null | undefined, @@ -10,3 +113,26 @@ export function getAcceptLanguagesWithGetter( export function getLocaleWithGetter(getter: () => string): Intl.Locale { return new Intl.Locale(getter()) } + +export function validateLocale(locale: string | Intl.Locale): void { + if ( + !(isLocale(locale) || + typeof locale === 'string' && validateLanguageTag(locale)) + ) { + throw new SyntaxError(`locale is invalid: ${locale.toString()}`) + } +} + +export function getExistCookies( + name: string, + getter: () => unknown, +) { + let setCookies = getter() + if (!Array.isArray(setCookies)) { + setCookies = [setCookies] + } + setCookies = (setCookies as string[]).filter((cookieValue: string) => + cookieValue && !cookieValue.startsWith(name + '=') + ) + return setCookies as string[] +} diff --git a/src/node.test.ts b/src/node.test.ts index c5f62dc..3f89609 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -1,6 +1,12 @@ import { describe, expect, test } from 'vitest' -import { getAcceptLanguages, getCookieLocale, getLocale } from './node.ts' -import { IncomingMessage } from 'node:http' +import supertest from 'supertest' +import { + getAcceptLanguages, + getCookieLocale, + getLocale, + setCookieLocale, +} from './node.ts' +import { createServer, IncomingMessage, OutgoingMessage } from 'node:http' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' describe('getAcceptLanguages', () => { @@ -132,3 +138,52 @@ describe('getCookieLocale', () => { .toThrowError(RangeError) }) }) + +describe('setCookieLocale', () => { + test('specify Locale instance', async () => { + const server = createServer((_req, res) => { + const locale = new Intl.Locale('ja-JP') + setCookieLocale(res, locale) + res.writeHead(200) + res.end('hello world!') + }) + const request = supertest(server) + const result = await request.get('/') + expect(result.headers['set-cookie']).toEqual([ + `${DEFAULT_COOKIE_NAME}=ja-JP; Path=/`, + ]) + }) + + test('specify language tag', async () => { + const server = createServer((_req, res) => { + setCookieLocale(res, 'ja-JP') + res.writeHead(200) + res.end('hello world!') + }) + const request = supertest(server) + const result = await request.get('/') + expect(result.headers['set-cookie']).toEqual([ + `${DEFAULT_COOKIE_NAME}=ja-JP; Path=/`, + ]) + }) + + test('specify cookie name', async () => { + const server = createServer((_req, res) => { + setCookieLocale(res, 'ja-JP', { name: 'intlify_locale' }) + res.writeHead(200) + res.end('hello world!') + }) + const request = supertest(server) + const result = await request.get('/') + expect(result.headers['set-cookie']).toEqual([ + 'intlify_locale=ja-JP; Path=/', + ]) + }) + + test('Syntax Error', () => { + const mockRes = {} as OutgoingMessage + + expect(() => setCookieLocale(mockRes, 'j')) + .toThrowError(/locale is invalid: j/) + }) +}) diff --git a/src/node.ts b/src/node.ts index 6bbad1b..af28ba3 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,8 +1,15 @@ -import { IncomingMessage } from 'node:http' -import { parse } from 'cookie-es' -import { getAcceptLanguagesWithGetter, getLocaleWithGetter } from './http.ts' +import { IncomingMessage, OutgoingMessage } from 'node:http' +import { parse, serialize } from 'cookie-es' +import { + getAcceptLanguagesWithGetter, + getExistCookies, + getLocaleWithGetter, + validateLocale, +} from './http.ts' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' +import type { CookieOptions } from './http.ts' + /** * get accpet languages * @@ -97,3 +104,42 @@ export function getCookieLocale( } return getLocaleWithGetter(getter) } + +/** + * set locale to the response `Set-Cookie` header. + * + * @example + * example for Node.js response: + * + * ```ts + * import { createServer } from 'node:http' + * import { setCookieLocale } from '@intlify/utils/node' + * + * const server = createServer((req, res) => { + * setCookieLocale(res, 'ja-JP') + * // ... + * }) + * ``` + * + * @param {OutgoingMessage} res The {@link OutgoingMessage | response} + * @param {string | Intl.Locale} locale The locale value + * @param {CookieOptions} options The cookie options, `name` option is `i18n_locale` as default, and `path` option is `/` as default. + * + * @throws {SyntaxError} Throws the {@link SyntaxError} if `locale` is invalid. + */ +export function setCookieLocale( + res: OutgoingMessage, + locale: string | Intl.Locale, + options: CookieOptions = { name: DEFAULT_COOKIE_NAME }, +): void { + validateLocale(locale) + const setCookies = getExistCookies( + options.name!, + () => res.getHeader('set-cookie'), + ) + const target = serialize(options.name!, locale.toString(), { + path: '/', + ...options, + }) + res.setHeader('set-cookie', [...setCookies, target]) +} diff --git a/src/web.test.ts b/src/web.test.ts index e9df43d..d5ddb1c 100644 --- a/src/web.test.ts +++ b/src/web.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from 'vitest' -import { getAcceptLanguages, getCookieLocale, getLocale } from './web.ts' +import { + getAcceptLanguages, + getCookieLocale, + getLocale, + setCookieLocale, +} from './web.ts' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' describe('getAcceptLanguages', () => { @@ -96,3 +101,36 @@ describe('getCookieLocale', () => { .toThrowError(RangeError) }) }) + +describe('setCookieLocale', () => { + test('specify Locale instance', () => { + const res = new Response('hello world!') + const locale = new Intl.Locale('ja-JP') + setCookieLocale(res, locale) + expect(res.headers.getSetCookie()).toEqual([ + `${DEFAULT_COOKIE_NAME}=ja-JP; Path=/`, + ]) + }) + + test('specify language tag', () => { + const res = new Response('hello world!') + setCookieLocale(res, 'ja-JP') + expect(res.headers.getSetCookie()).toEqual([ + `${DEFAULT_COOKIE_NAME}=ja-JP; Path=/`, + ]) + }) + + test('specify cookie name', () => { + const res = new Response('hello world!') + setCookieLocale(res, 'ja-JP', { name: 'intlify_locale' }) + expect(res.headers.getSetCookie()).toEqual([ + 'intlify_locale=ja-JP; Path=/', + ]) + }) + + test('Syntax Error', () => { + const res = new Response('hello world!') + expect(() => setCookieLocale(res, 'j')) + .toThrowError(/locale is invalid: j/) + }) +}) diff --git a/src/web.ts b/src/web.ts index 553f9de..382cb45 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,7 +1,14 @@ -import { parse } from 'cookie-es' -import { getAcceptLanguagesWithGetter, getLocaleWithGetter } from './http.ts' +import { parse, serialize } from 'cookie-es' +import { + getAcceptLanguagesWithGetter, + getExistCookies, + getLocaleWithGetter, + validateLocale, +} from './http.ts' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' +import type { CookieOptions } from './http.ts' + /** * get accpet languages * @@ -96,3 +103,46 @@ export function getCookieLocale( } return getLocaleWithGetter(getter) } + +/** + * set locale to the response `Set-Cookie` header. + * + * @example + * example for Web API response on Bun: + * + * ```ts + * import { setCookieLocale } from '@intlify/utils/web' + * + * Bun.serve({ + * port: 8080, + * fetch(req) { + * const res = new Response('こんにちは、世界!') + * setCookieLocale(res, 'ja-JP') + * // ... + * return res + * }, + * }) + * ``` + * + * @param {Response} res The {@link Response | response} + * @param {string | Intl.Locale} locale The locale value + * @param {CookieOptions} options The cookie options, `name` option is `i18n_locale` as default, and `path` option is `/` as default. + * + * @throws {SyntaxError} Throws the {@link SyntaxError} if `locale` is invalid. + */ +export function setCookieLocale( + res: Response, + locale: string | Intl.Locale, + options: CookieOptions = { name: DEFAULT_COOKIE_NAME }, +): void { + validateLocale(locale) + const setCookies = getExistCookies( + options.name!, + () => res.headers.getSetCookie(), + ) + const target = serialize(options.name!, locale.toString(), { + path: '/', + ...options, + }) + res.headers.set('set-cookie', [...setCookies, target].join('; ')) +}