Open4
Hono Adapter Helper の Next.js 版 (Next.js Adapter Helper) をメモする
概要
Hono.js の Adapter Helper や Next.js の cookies には、便利な関数が用意されている。
自分は、Web Standards を大事にしているが、MDN の Cookie とそれぞれミスマッチがある。
そこで、next/adapter なるものを自作することで、楽をしたい。
技術的には、以下を想定
Web Standards だと思っているので、
- session cookie で session のやり取りをする。
- server actions で form のやり取りをする。
=> cookie forwarding なる問題が発生していそう
frontend
- Next.js with server actions
- fetch with Session Cookie
backend
- Hono.js REST
- set Session Cookie
つまり、React <-> server actions (with Session Sookie) <-> Hono.js REST 間で API と cookie のやり取りがある。
server actions で ブラウザ?から cookie を取得して、fetch に cookie を埋め込むには?
(参考記事 にある Client to BackendのCookie Forwarding かも)
document.cookie -> Next.js fetch headers cookie 形式の変換。
next-app/src/libs/next/adapter/getHeadersCookie.ts
import { cookies } from 'next/headers'
/**
* Headers の cookie 文字列を取得する adapter 関数
*
* @param cookieNames {string[]} cookie.name の配列
*
* @example
* ```ts
* const headerCookie = getHeaderCookie(['__Host-session', 'test'])
* // __Host-session=abcde; test=12345 // for fetch headers.cookie
* // fetch(url, { headers: { cookie: headersCookie } })
* ```
*/
export function getHeadersCookie(cookieNames: string[]): string {
const headersCookie = cookies()
.getAll()
.flatMap((cookie) =>
cookieNames.includes(cookie.name)
? [`${cookie.name}=${cookie.value}`]
: [],
)
.join('; ')
return headersCookie
}
server actions で API のレスポンスから cookie を取得して、ブラウザ?に cookie を設定するには?
(参考記事 にある Backend to ClientのCookie Forwarding かも)
Hono.js (response set-cookie) -> Next.js cookies() で cookieString を set する。
next-app/src/libs/next/adapter/setCookies.ts
import { parseSetCookie } from '@/libs/set-cookie-parser'
import { cookies } from 'next/headers'
/**
* クッキーを設定する adapter 関数
*
* @param cookieString {string} response.headers.get('set-cookie')
*
* @example
* ```ts
* const response = await fetch()
* setCookies(response.headers.get('set-cookie') ?? '')
* ```
*/
export function setCookies(cookieString: string): void {
const cookieInfos = parseSetCookie(cookieString)
cookieInfos.map(({ name, options, value }) => {
cookies().set(name, value, options)
})
}
next-app/src/libs/set-cookie-parser/parseSetCookie.ts
import camelCase from 'just-camel-case'
// クッキーのオプションを表すインターフェース
interface CookieOptions {
[key: string]: string | boolean
}
// クッキーを表すインターフェース
interface Cookie {
name: string
value: string
options: CookieOptions
}
function splitCookiesString(cookieString: string): string[] {
// 改行または複数の Set-Cookie ヘッダーを考慮して分割
return cookieString.split(/\r?\n/).filter((line) => line.trim() !== '')
}
function parseCookie(cookieString: string): Cookie {
const [rawCookie, ...optionsParts] = cookieString.split(/; */)
const [name, value] = rawCookie.split('=')
const options = optionsParts.reduce((acc, part) => {
const [key, val] = part.split('=')
acc[camelCase(key)] = val ?? true
return acc
}, {} as CookieOptions)
return { name, value, options }
}
function parseCookies(cookiesArray: string[]): Cookie[] {
return cookiesArray.map((cookieString) => parseCookie(cookieString))
}
/**
* Set-Cookie ヘッダーの文字列をパースしてオブジェクトに変換する
*
* HACK: generated by GPT で、コードの質が低い
*
* ```ts
* const cookieString = `sessionId=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Path=/; Secure; HttpOnly
userId=xyz789; Path=/; Secure; HttpOnly`
* const cookies = parseSetCookie(cookieString)
* // [
* // {
* // name: "sessionId",
* // value: "abc123",
* // options: {
* // expires: "Wed, 09 Jun 2021 10:18:14 GMT",
* // path: "/",
* // secure: true,
* // httpOnly: true,
* // },
* // }, {
* // name: "userId",
* // value: "xyz789",
* // options: {
* // path: "/",
* // secure: true,
* // httpOnly: true,
* // },
* // }
* // ]
* ```
*/
export function parseSetCookie(cookieString: string): Cookie[] {
const cookiesArray = splitCookiesString(cookieString)
return parseCookies(cookiesArray)
}