diff --git a/app/components/KakaoLoginButton/KakaoLoginButton.tsx b/app/components/KakaoLoginButton/KakaoLoginButton.tsx index eded8da..9ca08d5 100644 --- a/app/components/KakaoLoginButton/KakaoLoginButton.tsx +++ b/app/components/KakaoLoginButton/KakaoLoginButton.tsx @@ -1,15 +1,18 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useState } from 'react'; +import useEnv from '@/hooks/useEnv'; import { getUUID } from '@/utils/random'; -const clientId = 'f5aa2f20e42d783654b8e8c01bfc6312'; -//redirectUri는 등록된 redirectUri중에 임의로 사용했습니다. -const redirectUri = 'http://localhost:5173/oauth/kakao'; - const KakaoLoginButton: React.FC = () => { - const kakaoAuthUrl = useMemo(() => { + const env = useEnv(); + const [kakaoAuthUrl, setKakaoAuthUrl] = useState(''); + + useEffect(() => { const userUUID = getUUID(); - return `https://kauth.kakao.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${userUUID}`; - }, []); + const redirectUri = window.location.origin + '/oauth/kakao'; + setKakaoAuthUrl( + `https://kauth.kakao.com/oauth/authorize?client_id=${env.KAKAO_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=code&state=${userUUID}`, + ); + }, [env.KAKAO_CLIENT_ID]); return ( diff --git a/app/contexts/EnvContext.tsx b/app/contexts/EnvContext.tsx new file mode 100644 index 0000000..ceb7313 --- /dev/null +++ b/app/contexts/EnvContext.tsx @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { type ClientEnv } from '@server/constants/env'; + +export const EnvContext = createContext(null); diff --git a/app/hooks/useEnv.tsx b/app/hooks/useEnv.tsx new file mode 100644 index 0000000..c470b07 --- /dev/null +++ b/app/hooks/useEnv.tsx @@ -0,0 +1,21 @@ +import { useContext } from 'react'; +import { EnvContext } from '@/contexts/EnvContext'; +import { type ClientEnv, clientEnvSchema } from '@server/constants/env'; + +/** + * 클라이언트용 환경 변수를 얻는 hook + * @example + * ``` + * const env = useEnv(); + * console.log(env.KAKAO_CLIENT_ID); + * ``` + */ +const useEnv = (): ClientEnv => { + const envFromContext = useContext(EnvContext); + + const env = clientEnvSchema.parse(envFromContext); + + return env; +}; + +export default useEnv; diff --git a/app/root.tsx b/app/root.tsx index ecbfa15..2854c90 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,31 +1,42 @@ +import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare'; import { Links, Meta, Outlet, Scripts, ScrollRestoration, + useLoaderData, } from '@remix-run/react'; -import { type ReactNode } from 'react'; +import { EnvContext } from '@/contexts/EnvContext'; import './root.css'; import '@/styles/theme.css'; -export const Layout = ({ children }: { children: ReactNode }) => ( - - - - - - - - - {children} - - - - -); +export const loader = async (args: LoaderFunctionArgs) => + json({ + env: args.context.clientEnv, + }); -const App = () => ; +const App = () => { + const data = useLoaderData(); + + return ( + + + + + + + + + + + + + + + + ); +}; export default App; diff --git a/app/routes/oauth.kakao.tsx b/app/routes/oauth.kakao.tsx new file mode 100644 index 0000000..c182ef8 --- /dev/null +++ b/app/routes/oauth.kakao.tsx @@ -0,0 +1,15 @@ +import { type LoaderFunctionArgs } from '@remix-run/cloudflare'; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const url = new URL(request.url); + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + + console.log(code, state); + + return null; +}; + +const KakaoRedirect = () =>
카카오 로그인 중...
; + +export default KakaoRedirect; diff --git a/app/routes/oauth.kakao.tsx.tsx b/app/routes/oauth.kakao.tsx.tsx deleted file mode 100644 index e7eeba1..0000000 --- a/app/routes/oauth.kakao.tsx.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { LoaderFunction, json } from '@remix-run/cloudflare'; - -export const loader: LoaderFunction = async ({ request }) => { - const url = new URL(request.url); - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - - // 이 아래의 try문 지우고 이어서 하면 됩니다 - try { - console.log(code, state); - } catch { - console.log(json); - } -}; - -const KakaoRedirect = () =>
카카오 로그인 중...
; - -export default KakaoRedirect; diff --git a/app/vite-env.d.ts b/app/vite-env.d.ts deleted file mode 100644 index 7ed3a44..0000000 --- a/app/vite-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// - -interface ImportMetaEnv { - readonly VITE_KAKAO_CLIENT_ID: string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} diff --git a/functions/[[path]].ts b/functions/[[path]].ts index 4d374ad..2e242ff 100644 --- a/functions/[[path]].ts +++ b/functions/[[path]].ts @@ -1,59 +1,34 @@ -import { - createWorkersKVSessionStorage, - createCookie, -} from '@remix-run/cloudflare'; import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages'; -import type { FetchApi } from '@/constants/types/api'; -import type { AuthSession, AuthSessionData } from '@/constants/types/auth'; -import { fetchApi } from '@/utils/api.server'; +import { + clientEnvSchema, + type ContextEnv, + serverEnvSchema, +} from '@server/constants/env'; +import { getLoadContext, makeAuthSession } from '@server/utils/cloudflare'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - the server build file is generated by `remix vite:build` // eslint-disable-next-line import/no-unresolved, import/order import * as build from '../build/server'; -type ContextEnv = { - readonly KV_NAMESPACE: KVNamespace; - readonly AUTH_COOKIE_SESSION_SECRET: string; - readonly API_URL: string; -}; - -declare module '@remix-run/cloudflare' { - interface AppLoadContext extends ContextEnv { - authSession: AuthSession; - fetchApi: FetchApi; - } -} - export const onRequest = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any context: EventContext, ): Promise => { - const authSessionCookie = createCookie('__auth_session', { - secrets: [context.env.AUTH_COOKIE_SESSION_SECRET], - sameSite: true, - }); - const authSessionStorage = createWorkersKVSessionStorage({ - cookie: authSessionCookie, - kv: context.env.KV_NAMESPACE, - }); - const authSession = await authSessionStorage.getSession( + clientEnvSchema.parse(context.env); + serverEnvSchema.parse(context.env); + + const { authSessionStorage, authSession } = await makeAuthSession( + { + authCookieSessionSecret: context.env.AUTH_COOKIE_SESSION_SECRET, + kvNamespace: context.env.KV_NAMESPACE, + }, context.request.headers.get('Cookie'), ); const handleRequest = createPagesFunctionHandler({ build, - getLoadContext: ({ context: loadContext }) => ({ - ...loadContext.cloudflare.env, - authSession, - fetchApi: async (apiInfo, variables) => - fetchApi( - apiInfo, - variables, - loadContext.cloudflare.env.API_URL, - authSession, - ), - }), + getLoadContext: (args) => getLoadContext(authSession, args), }); const response = await handleRequest(context); diff --git a/package.json b/package.json index ec2bddf..00a2421 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "type": "module", "scripts": { "build": "remix vite:build", - "dev": "wrangler pages download config ort-web --force && remix vite:build && wrangler pages dev --live-reload ./build/client", + "dev": "wrangler pages download config ort-web --force && remix vite:dev", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "start": "remix-serve ./build/server/index.js", "typecheck": "tsc", @@ -24,12 +24,14 @@ "isbot": "4.4.0", "react": "18.3.1", "react-dom": "18.3.1", - "uuid": "10.0.0" + "uuid": "10.0.0", + "zod": "3.23.8" }, "devDependencies": { "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "19.2.2", "@remix-run/dev": "2.11.2", + "@remix-run/node": "2.11.2", "@remix-run/testing": "2.11.2", "@storybook/addon-essentials": "8.2.9", "@storybook/addon-interactions": "8.2.9", @@ -40,6 +42,7 @@ "@storybook/test": "8.2.9", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", + "@types/set-cookie-parser": "2.4.10", "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "7.16.0", "@typescript-eslint/parser": "7.16.0", @@ -56,6 +59,7 @@ "husky": "9.1.1", "lint-staged": "15.2.7", "prettier": "3.3.3", + "set-cookie-parser": "2.7.0", "storybook": "8.2.9", "typescript": "5.5.3", "vite": "5.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7170ef4..b85e08a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,10 @@ importers: dependencies: '@remix-run/cloudflare': specifier: 2.11.2 - version: 2.11.2(@cloudflare/workers-types@4.20240821.1)(typescript@5.5.3) + version: 2.11.2(@cloudflare/workers-types@4.20240903.0)(typescript@5.5.3) '@remix-run/cloudflare-pages': specifier: 2.11.2 - version: 2.11.2(@cloudflare/workers-types@4.20240821.1)(typescript@5.5.3) + version: 2.11.2(@cloudflare/workers-types@4.20240903.0)(typescript@5.5.3) '@remix-run/react': specifier: 2.11.2 version: 2.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3) @@ -35,16 +35,22 @@ importers: uuid: specifier: 10.0.0 version: 10.0.0 + zod: + specifier: 3.23.8 + version: 3.23.8 devDependencies: '@commitlint/cli': specifier: 19.3.0 - version: 19.3.0(@types/node@22.5.1)(typescript@5.5.3) + version: 19.3.0(@types/node@22.5.3)(typescript@5.5.3) '@commitlint/config-conventional': specifier: 19.2.2 version: 19.2.2 '@remix-run/dev': specifier: 2.11.2 - version: 2.11.2(@remix-run/react@2.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/serve@2.11.2(typescript@5.5.3))(@types/node@22.5.1)(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1))(wrangler@3.72.2(@cloudflare/workers-types@4.20240821.1)) + version: 2.11.2(@remix-run/react@2.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/serve@2.11.2(typescript@5.5.3))(@types/node@22.5.3)(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3))(wrangler@3.72.2(@cloudflare/workers-types@4.20240903.0)) + '@remix-run/node': + specifier: 2.11.2 + version: 2.11.2(typescript@5.5.3) '@remix-run/testing': specifier: 2.11.2 version: 2.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3) @@ -65,7 +71,7 @@ importers: version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3) '@storybook/react-vite': specifier: 8.2.9 - version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.2)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1)) + version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.2)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3)) '@storybook/test': specifier: 8.2.9 version: 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))) @@ -75,6 +81,9 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + '@types/set-cookie-parser': + specifier: 2.4.10 + version: 2.4.10 '@types/uuid': specifier: 10.0.0 version: 10.0.0 @@ -86,10 +95,10 @@ importers: version: 7.16.0(eslint@8.57.0)(typescript@5.5.3) '@vanilla-extract/vite-plugin': specifier: 4.0.13 - version: 4.0.13(@types/node@22.5.1)(vite@5.3.3(@types/node@22.5.1)) + version: 4.0.13(@types/node@22.5.3)(vite@5.3.3(@types/node@22.5.3)) autoprefixer: specifier: 10.4.19 - version: 10.4.19(postcss@8.4.41) + version: 10.4.19(postcss@8.4.45) eslint: specifier: 8.57.0 version: 8.57.0 @@ -123,6 +132,9 @@ importers: prettier: specifier: 3.3.3 version: 3.3.3 + set-cookie-parser: + specifier: 2.7.0 + version: 2.7.0 storybook: specifier: 8.2.9 version: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)) @@ -131,13 +143,13 @@ importers: version: 5.5.3 vite: specifier: 5.3.3 - version: 5.3.3(@types/node@22.5.1) + version: 5.3.3(@types/node@22.5.3) vite-tsconfig-paths: specifier: 4.3.2 - version: 4.3.2(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1)) + version: 4.3.2(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3)) wrangler: specifier: 3.72.2 - version: 3.72.2(@cloudflare/workers-types@4.20240821.1) + version: 3.72.2(@cloudflare/workers-types@4.20240903.0) packages: @@ -818,8 +830,8 @@ packages: resolution: {integrity: sha512-cqtLW1QiBC/ABaZIhAdyGCsnHHY6pAb6hsVUZg82Co2gQtf/faxRYV1FgpCwUYroTdk6A66xUMSTmFqreKCJow==} engines: {node: '>=16.7.0'} - '@cloudflare/workers-types@4.20240821.1': - resolution: {integrity: sha512-icAkbnAqgVl6ef9lgLTom8na+kj2RBw2ViPAQ586hbdj0xZcnrjK7P46Eu08OU9D/lNDgN2sKU/sxhe2iK/gIg==} + '@cloudflare/workers-types@4.20240903.0': + resolution: {integrity: sha512-a4mqgtVsPWg3JNNlQdLRE0Z6/mHr/uXa1ANDw6Zd7in438UCbeb+j7Z954Sf93G24jExpAn9VZ8kUUml0RwZbQ==} '@commitlint/cli@19.3.0': resolution: {integrity: sha512-LgYWOwuDR7BSTQ9OLZ12m7F/qhNY+NpAyPBgo4YNMkACE7lGuUnuQq1yi9hz1KA4+3VqpOYl8H1rY/LYK43v7g==} @@ -2094,11 +2106,11 @@ packages: '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} - '@types/node@18.19.47': - resolution: {integrity: sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==} + '@types/node@18.19.49': + resolution: {integrity: sha512-ALCeIR6n0nQ7j0FUF1ycOhrp6+XutJWqEu/vtdEqXFUQwkBfgUA5cEg3ZNmjWGF/ZYA/FcF9QMkL55Ar0O6UrA==} - '@types/node@22.5.1': - resolution: {integrity: sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==} + '@types/node@22.5.3': + resolution: {integrity: sha512-njripolh85IA9SQGTAqbmnNZTdxv7X/4OYGPz8tgy5JDr8MP+uDBa921GpYEoDDnwm0Hmn5ZPeJgiiSTPoOzkQ==} '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -2127,6 +2139,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3062,8 +3077,8 @@ packages: eslint: '*' eslint-plugin-import: '*' - eslint-module-utils@2.8.2: - resolution: {integrity: sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==} + eslint-module-utils@2.9.0: + resolution: {integrity: sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -3313,8 +3328,8 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - flow-parser@0.245.0: - resolution: {integrity: sha512-xUBkkpIDfDZHAebnDEX65FCVitJUctab82KFmtP5SY4cGly1vbuYNe6Muyp0NLXrgmBChVdoC2T+3/RUHi4Mww==} + flow-parser@0.245.1: + resolution: {integrity: sha512-KaVIjRdCY+APtxQijfV1c7GN1bofByIlR7E6omQLW0sghkA8hh8uufQOqTf3oAAVTExsSLafmdL/QwyvE/gdEg==} engines: {node: '>=0.4.0'} for-each@0.3.3: @@ -4626,8 +4641,8 @@ packages: periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -4719,8 +4734,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.41: - resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} + postcss@8.4.45: + resolution: {integrity: sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -5385,8 +5400,8 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - tsconfck@3.1.1: - resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==} + tsconfck@3.1.3: + resolution: {integrity: sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==} engines: {node: ^18 || >=20} hasBin: true peerDependencies: @@ -5561,9 +5576,14 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin@1.12.2: - resolution: {integrity: sha512-bEqQxeC7rxtxPZ3M5V4Djcc4lQqKPgGe3mAWZvxcSmX5jhGxll19NliaRzQSQPrk4xJZSGniK3puLWpRuZN7VQ==} + unplugin@1.13.1: + resolution: {integrity: sha512-6Kq1iSSwg7KyjcThRUks9LuqDAKvtnioxbL9iEtB9ctTyBA5OmrB8gZd/d225VJu1w3UpUsKV7eGrvf59J7+VA==} engines: {node: '>=14.0.0'} + peerDependencies: + webpack-sources: ^3 + peerDependenciesMeta: + webpack-sources: + optional: true update-browserslist-db@1.1.0: resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} @@ -5668,10 +5688,6 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} - webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -5783,8 +5799,8 @@ packages: engines: {node: '>= 14'} hasBin: true - yaml@2.5.0: - resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} + yaml@2.5.1: + resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} engines: {node: '>= 14'} hasBin: true @@ -5825,7 +5841,7 @@ snapshots: '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 - picocolors: 1.0.1 + picocolors: 1.1.0 '@babel/compat-data@7.25.4': {} @@ -5992,7 +6008,7 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.1 + picocolors: 1.1.0 '@babel/parser@7.25.6': dependencies: @@ -6674,13 +6690,13 @@ snapshots: '@cloudflare/workers-shared@0.3.0': {} - '@cloudflare/workers-types@4.20240821.1': {} + '@cloudflare/workers-types@4.20240903.0': {} - '@commitlint/cli@19.3.0(@types/node@22.5.1)(typescript@5.5.3)': + '@commitlint/cli@19.3.0(@types/node@22.5.3)(typescript@5.5.3)': dependencies: '@commitlint/format': 19.3.0 '@commitlint/lint': 19.4.1 - '@commitlint/load': 19.4.0(@types/node@22.5.1)(typescript@5.5.3) + '@commitlint/load': 19.4.0(@types/node@22.5.3)(typescript@5.5.3) '@commitlint/read': 19.4.0 '@commitlint/types': 19.0.3 execa: 8.0.1 @@ -6727,7 +6743,7 @@ snapshots: '@commitlint/rules': 19.4.1 '@commitlint/types': 19.0.3 - '@commitlint/load@19.4.0(@types/node@22.5.1)(typescript@5.5.3)': + '@commitlint/load@19.4.0(@types/node@22.5.3)(typescript@5.5.3)': dependencies: '@commitlint/config-validator': 19.0.3 '@commitlint/execute-rule': 19.0.0 @@ -6735,7 +6751,7 @@ snapshots: '@commitlint/types': 19.0.3 chalk: 5.3.0 cosmiconfig: 9.0.0(typescript@5.5.3) - cosmiconfig-typescript-loader: 5.0.0(@types/node@22.5.1)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3) + cosmiconfig-typescript-loader: 5.0.0(@types/node@22.5.3)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -7126,13 +7142,13 @@ snapshots: dependencies: '@sinclair/typebox': 0.27.8 - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3))': dependencies: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.5.3) - vite: 5.3.3(@types/node@22.5.1) + vite: 5.3.3(@types/node@22.5.3) optionalDependencies: typescript: 5.5.3 @@ -7238,22 +7254,22 @@ snapshots: '@pkgr/core@0.1.1': {} - '@remix-run/cloudflare-pages@2.11.2(@cloudflare/workers-types@4.20240821.1)(typescript@5.5.3)': + '@remix-run/cloudflare-pages@2.11.2(@cloudflare/workers-types@4.20240903.0)(typescript@5.5.3)': dependencies: - '@cloudflare/workers-types': 4.20240821.1 - '@remix-run/cloudflare': 2.11.2(@cloudflare/workers-types@4.20240821.1)(typescript@5.5.3) + '@cloudflare/workers-types': 4.20240903.0 + '@remix-run/cloudflare': 2.11.2(@cloudflare/workers-types@4.20240903.0)(typescript@5.5.3) optionalDependencies: typescript: 5.5.3 - '@remix-run/cloudflare@2.11.2(@cloudflare/workers-types@4.20240821.1)(typescript@5.5.3)': + '@remix-run/cloudflare@2.11.2(@cloudflare/workers-types@4.20240903.0)(typescript@5.5.3)': dependencies: '@cloudflare/kv-asset-handler': 0.1.3 - '@cloudflare/workers-types': 4.20240821.1 + '@cloudflare/workers-types': 4.20240903.0 '@remix-run/server-runtime': 2.11.2(typescript@5.5.3) optionalDependencies: typescript: 5.5.3 - '@remix-run/dev@2.11.2(@remix-run/react@2.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/serve@2.11.2(typescript@5.5.3))(@types/node@22.5.1)(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1))(wrangler@3.72.2(@cloudflare/workers-types@4.20240821.1))': + '@remix-run/dev@2.11.2(@remix-run/react@2.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/serve@2.11.2(typescript@5.5.3))(@types/node@22.5.3)(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3))(wrangler@3.72.2(@cloudflare/workers-types@4.20240903.0))': dependencies: '@babel/core': 7.25.2 '@babel/generator': 7.25.6 @@ -7270,7 +7286,7 @@ snapshots: '@remix-run/router': 1.19.1 '@remix-run/server-runtime': 2.11.2(typescript@5.5.3) '@types/mdx': 2.0.13 - '@vanilla-extract/integration': 6.5.0(@types/node@22.5.1) + '@vanilla-extract/integration': 6.5.0(@types/node@22.5.3) arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 @@ -7292,13 +7308,13 @@ snapshots: lodash.debounce: 4.0.8 minimatch: 9.0.5 ora: 5.4.1 - picocolors: 1.0.1 + picocolors: 1.1.0 picomatch: 2.3.1 pidtree: 0.6.0 - postcss: 8.4.41 - postcss-discard-duplicates: 5.1.0(postcss@8.4.41) - postcss-load-config: 4.0.2(postcss@8.4.41) - postcss-modules: 6.0.0(postcss@8.4.41) + postcss: 8.4.45 + postcss-discard-duplicates: 5.1.0(postcss@8.4.45) + postcss-load-config: 4.0.2(postcss@8.4.45) + postcss-modules: 6.0.0(postcss@8.4.45) prettier: 2.8.8 pretty-ms: 7.0.1 react-refresh: 0.14.2 @@ -7312,8 +7328,8 @@ snapshots: optionalDependencies: '@remix-run/serve': 2.11.2(typescript@5.5.3) typescript: 5.5.3 - vite: 5.3.3(@types/node@22.5.1) - wrangler: 3.72.2(@cloudflare/workers-types@4.20240821.1) + vite: 5.3.3(@types/node@22.5.3) + wrangler: 3.72.2(@cloudflare/workers-types@4.20240903.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7529,6 +7545,7 @@ snapshots: ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color + - webpack-sources '@storybook/addon-essentials@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))': dependencies: @@ -7545,6 +7562,7 @@ snapshots: ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color + - webpack-sources '@storybook/addon-highlight@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))': dependencies: @@ -7617,7 +7635,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1))': + '@storybook/builder-vite@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3))': dependencies: '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))) '@types/find-cache-dir': 3.2.1 @@ -7629,11 +7647,12 @@ snapshots: magic-string: 0.30.11 storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)) ts-dedent: 2.2.0 - vite: 5.3.3(@types/node@22.5.1) + vite: 5.3.3(@types/node@22.5.3) optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color + - webpack-sources '@storybook/codemod@8.2.9': dependencies: @@ -7663,7 +7682,7 @@ snapshots: dependencies: '@storybook/csf': 0.1.11 '@types/express': 4.17.21 - '@types/node': 18.19.47 + '@types/node': 18.19.49 browser-assert: 1.2.1 esbuild: 0.21.5 esbuild-register: 3.6.0(esbuild@0.21.5) @@ -7680,7 +7699,9 @@ snapshots: '@storybook/csf-plugin@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))': dependencies: storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)) - unplugin: 1.12.2 + unplugin: 1.13.1 + transitivePeerDependencies: + - webpack-sources '@storybook/csf@0.0.1': dependencies: @@ -7718,11 +7739,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)) - '@storybook/react-vite@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.2)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1))': + '@storybook/react-vite@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.2)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3)) '@rollup/pluginutils': 5.1.0(rollup@4.21.2) - '@storybook/builder-vite': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1)) + '@storybook/builder-vite': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3)) '@storybook/react': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3) find-up: 5.0.0 magic-string: 0.30.11 @@ -7732,13 +7753,14 @@ snapshots: resolve: 1.22.8 storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)) tsconfig-paths: 4.2.0 - vite: 5.3.3(@types/node@22.5.1) + vite: 5.3.3(@types/node@22.5.3) transitivePeerDependencies: - '@preact/preset-vite' - rollup - supports-color - typescript - vite-plugin-glimmerx + - webpack-sources '@storybook/react@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(typescript@5.5.3)': dependencies: @@ -7750,7 +7772,7 @@ snapshots: '@storybook/theming': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))) '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 - '@types/node': 18.19.47 + '@types/node': 18.19.49 acorn: 7.4.1 acorn-jsx: 5.3.2(acorn@7.4.1) acorn-walk: 7.2.0 @@ -7847,21 +7869,21 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 18.19.47 + '@types/node': 18.19.49 '@types/connect@3.4.38': dependencies: - '@types/node': 18.19.47 + '@types/node': 18.19.49 '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 22.5.1 + '@types/node': 22.5.3 '@types/cookie@0.6.0': {} '@types/cross-spawn@6.0.6': dependencies: - '@types/node': 22.5.1 + '@types/node': 22.5.3 '@types/debug@4.1.12': dependencies: @@ -7883,7 +7905,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 18.19.47 + '@types/node': 18.19.49 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -7900,7 +7922,7 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.5.1 + '@types/node': 22.5.3 '@types/hast@2.3.10': dependencies: @@ -7932,13 +7954,13 @@ snapshots: '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.5.1 + '@types/node': 22.5.3 - '@types/node@18.19.47': + '@types/node@18.19.49': dependencies: undici-types: 5.26.5 - '@types/node@22.5.1': + '@types/node@22.5.3': dependencies: undici-types: 6.19.8 @@ -7964,14 +7986,18 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 18.19.47 + '@types/node': 18.19.49 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 18.19.47 + '@types/node': 18.19.49 '@types/send': 0.17.4 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 22.5.3 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8122,7 +8148,7 @@ snapshots: deepmerge: 4.3.1 media-query-parser: 2.0.2 modern-ahocorasick: 1.0.1 - picocolors: 1.0.1 + picocolors: 1.1.0 transitivePeerDependencies: - babel-plugin-macros @@ -8139,25 +8165,25 @@ snapshots: lru-cache: 10.4.3 media-query-parser: 2.0.2 modern-ahocorasick: 1.0.1 - picocolors: 1.0.1 + picocolors: 1.1.0 transitivePeerDependencies: - babel-plugin-macros - '@vanilla-extract/integration@6.5.0(@types/node@22.5.1)': + '@vanilla-extract/integration@6.5.0(@types/node@22.5.3)': dependencies: '@babel/core': 7.25.2 '@babel/plugin-syntax-typescript': 7.25.4(@babel/core@7.25.2) '@vanilla-extract/babel-plugin-debug-ids': 1.0.6 '@vanilla-extract/css': 1.15.3 - esbuild: 0.17.6 + esbuild: 0.17.19 eval: 0.1.8 find-up: 5.0.0 javascript-stringify: 2.1.0 lodash: 4.17.21 mlly: 1.7.1 outdent: 0.8.0 - vite: 5.3.3(@types/node@22.5.1) - vite-node: 1.6.0(@types/node@22.5.1) + vite: 5.3.3(@types/node@22.5.3) + vite-node: 1.6.0(@types/node@22.5.3) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -8169,7 +8195,7 @@ snapshots: - supports-color - terser - '@vanilla-extract/integration@7.1.9(@types/node@22.5.1)': + '@vanilla-extract/integration@7.1.9(@types/node@22.5.3)': dependencies: '@babel/core': 7.25.2 '@babel/plugin-syntax-typescript': 7.25.4(@babel/core@7.25.2) @@ -8181,8 +8207,8 @@ snapshots: find-up: 5.0.0 javascript-stringify: 2.1.0 mlly: 1.7.1 - vite: 5.3.3(@types/node@22.5.1) - vite-node: 1.6.0(@types/node@22.5.1) + vite: 5.3.3(@types/node@22.5.3) + vite-node: 1.6.0(@types/node@22.5.3) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -8196,10 +8222,10 @@ snapshots: '@vanilla-extract/private@1.0.6': {} - '@vanilla-extract/vite-plugin@4.0.13(@types/node@22.5.1)(vite@5.3.3(@types/node@22.5.1))': + '@vanilla-extract/vite-plugin@4.0.13(@types/node@22.5.3)(vite@5.3.3(@types/node@22.5.3))': dependencies: - '@vanilla-extract/integration': 7.1.9(@types/node@22.5.1) - vite: 5.3.3(@types/node@22.5.1) + '@vanilla-extract/integration': 7.1.9(@types/node@22.5.3) + vite: 5.3.3(@types/node@22.5.3) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -8423,14 +8449,14 @@ snapshots: astring@1.9.0: {} - autoprefixer@10.4.19(postcss@8.4.41): + autoprefixer@10.4.19(postcss@8.4.45): dependencies: browserslist: 4.23.3 caniuse-lite: 1.0.30001655 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.41 + picocolors: 1.1.0 + postcss: 8.4.45 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -8760,9 +8786,9 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@5.0.0(@types/node@22.5.1)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3): + cosmiconfig-typescript-loader@5.0.0(@types/node@22.5.3)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3): dependencies: - '@types/node': 22.5.1 + '@types/node': 22.5.3 cosmiconfig: 9.0.0(typescript@5.5.3) jiti: 1.21.6 typescript: 5.5.3 @@ -9217,7 +9243,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.0 @@ -9229,7 +9255,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.9.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -9250,7 +9276,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -9449,7 +9475,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 22.5.1 + '@types/node': 22.5.3 require-like: 0.1.2 event-target-shim@5.0.1: {} @@ -9610,7 +9636,7 @@ snapshots: flatted@3.3.1: {} - flow-parser@0.245.0: {} + flow-parser@0.245.1: {} for-each@0.3.3: dependencies: @@ -9892,9 +9918,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.4.41): + icss-utils@5.1.0(postcss@8.4.45): dependencies: - postcss: 8.4.41 + postcss: 8.4.45 ieee754@1.2.1: {} @@ -10136,7 +10162,7 @@ snapshots: '@babel/register': 7.24.6(@babel/core@7.25.2) babel-core: 7.0.0-bridge.0(@babel/core@7.25.2) chalk: 4.1.2 - flow-parser: 0.245.0 + flow-parser: 0.245.1 graceful-fs: 4.2.11 micromatch: 4.0.8 neo-async: 2.6.2 @@ -11076,7 +11102,7 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.2 - picocolors@1.0.1: {} + picocolors@1.1.0: {} picomatch@2.3.1: {} @@ -11106,48 +11132,48 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-discard-duplicates@5.1.0(postcss@8.4.41): + postcss-discard-duplicates@5.1.0(postcss@8.4.45): dependencies: - postcss: 8.4.41 + postcss: 8.4.45 - postcss-load-config@4.0.2(postcss@8.4.41): + postcss-load-config@4.0.2(postcss@8.4.45): dependencies: lilconfig: 3.1.2 - yaml: 2.5.0 + yaml: 2.5.1 optionalDependencies: - postcss: 8.4.41 + postcss: 8.4.45 - postcss-modules-extract-imports@3.1.0(postcss@8.4.41): + postcss-modules-extract-imports@3.1.0(postcss@8.4.45): dependencies: - postcss: 8.4.41 + postcss: 8.4.45 - postcss-modules-local-by-default@4.0.5(postcss@8.4.41): + postcss-modules-local-by-default@4.0.5(postcss@8.4.45): dependencies: - icss-utils: 5.1.0(postcss@8.4.41) - postcss: 8.4.41 + icss-utils: 5.1.0(postcss@8.4.45) + postcss: 8.4.45 postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.0(postcss@8.4.41): + postcss-modules-scope@3.2.0(postcss@8.4.45): dependencies: - postcss: 8.4.41 + postcss: 8.4.45 postcss-selector-parser: 6.1.2 - postcss-modules-values@4.0.0(postcss@8.4.41): + postcss-modules-values@4.0.0(postcss@8.4.45): dependencies: - icss-utils: 5.1.0(postcss@8.4.41) - postcss: 8.4.41 + icss-utils: 5.1.0(postcss@8.4.45) + postcss: 8.4.45 - postcss-modules@6.0.0(postcss@8.4.41): + postcss-modules@6.0.0(postcss@8.4.45): dependencies: generic-names: 4.0.0 - icss-utils: 5.1.0(postcss@8.4.41) + icss-utils: 5.1.0(postcss@8.4.45) lodash.camelcase: 4.3.0 - postcss: 8.4.41 - postcss-modules-extract-imports: 3.1.0(postcss@8.4.41) - postcss-modules-local-by-default: 4.0.5(postcss@8.4.41) - postcss-modules-scope: 3.2.0(postcss@8.4.41) - postcss-modules-values: 4.0.0(postcss@8.4.41) + postcss: 8.4.45 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.45) + postcss-modules-local-by-default: 4.0.5(postcss@8.4.45) + postcss-modules-scope: 3.2.0(postcss@8.4.45) + postcss-modules-values: 4.0.0(postcss@8.4.45) string-hash: 1.1.3 postcss-selector-parser@6.1.2: @@ -11157,10 +11183,10 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.4.41: + postcss@8.4.45: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 + picocolors: 1.1.0 source-map-js: 1.2.0 prelude-ls@1.2.1: {} @@ -11944,7 +11970,7 @@ snapshots: ts-dedent@2.2.0: {} - tsconfck@3.1.1(typescript@5.5.3): + tsconfck@3.1.3(typescript@5.5.3): optionalDependencies: typescript: 5.5.3 @@ -12139,18 +12165,16 @@ snapshots: unpipe@1.0.0: {} - unplugin@1.12.2: + unplugin@1.13.1: dependencies: acorn: 8.12.1 - chokidar: 3.6.0 - webpack-sources: 3.2.3 webpack-virtual-modules: 0.6.2 update-browserslist-db@1.1.0(browserslist@4.23.3): dependencies: browserslist: 4.23.3 escalade: 3.2.0 - picocolors: 1.0.1 + picocolors: 1.1.0 uri-js@4.4.1: dependencies: @@ -12200,13 +12224,13 @@ snapshots: unist-util-stringify-position: 3.0.3 vfile-message: 3.1.4 - vite-node@1.6.0(@types/node@22.5.1): + vite-node@1.6.0(@types/node@22.5.3): dependencies: cac: 6.7.14 debug: 4.3.6 pathe: 1.1.2 - picocolors: 1.0.1 - vite: 5.3.3(@types/node@22.5.1) + picocolors: 1.1.0 + vite: 5.3.3(@types/node@22.5.3) transitivePeerDependencies: - '@types/node' - less @@ -12217,24 +12241,24 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@4.3.2(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.1)): + vite-tsconfig-paths@4.3.2(typescript@5.5.3)(vite@5.3.3(@types/node@22.5.3)): dependencies: debug: 4.3.6 globrex: 0.1.2 - tsconfck: 3.1.1(typescript@5.5.3) + tsconfck: 3.1.3(typescript@5.5.3) optionalDependencies: - vite: 5.3.3(@types/node@22.5.1) + vite: 5.3.3(@types/node@22.5.3) transitivePeerDependencies: - supports-color - typescript - vite@5.3.3(@types/node@22.5.1): + vite@5.3.3(@types/node@22.5.3): dependencies: esbuild: 0.21.5 - postcss: 8.4.41 + postcss: 8.4.45 rollup: 4.21.2 optionalDependencies: - '@types/node': 22.5.1 + '@types/node': 22.5.3 fsevents: 2.3.3 walk-up-path@3.0.1: {} @@ -12251,8 +12275,6 @@ snapshots: web-streams-polyfill@3.3.3: {} - webpack-sources@3.2.3: {} - webpack-virtual-modules@0.6.2: {} which-boxed-primitive@1.0.2: @@ -12311,7 +12333,7 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20240821.1 '@cloudflare/workerd-windows-64': 1.20240821.1 - wrangler@3.72.2(@cloudflare/workers-types@4.20240821.1): + wrangler@3.72.2(@cloudflare/workers-types@4.20240903.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@cloudflare/workers-shared': 0.3.0 @@ -12332,7 +12354,7 @@ snapshots: workerd: 1.20240821.1 xxhash-wasm: 1.0.2 optionalDependencies: - '@cloudflare/workers-types': 4.20240821.1 + '@cloudflare/workers-types': 4.20240903.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -12381,7 +12403,7 @@ snapshots: yaml@2.4.5: {} - yaml@2.5.0: {} + yaml@2.5.1: {} yargs-parser@21.1.1: {} diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..3aa2c1c --- /dev/null +++ b/server/README.md @@ -0,0 +1,67 @@ +## About + +`server` 디렉토리는 Cloudflare 런타임 환경에 돌아갈 기능 등 서버와 관련이 있는 코드가 있는 곳이다. + +## ⚠️ Caution: Absolute Path + +`server` 내에서는 절대 경로(absolute path)를 사용하여 import하면 안 된다. + +Vite 플러그인 등 절대 경로가 적용되기 전에 평가되어야 하는 코드가 있기 때문에, 절대 경로를 사용하면 module resolution 오류가 발생한다. + +```typescript +// error +import { clientEnvSchema } from '@server/constants/env'; + +// ok +import { clientEnvSchema } from '../constants/env'; +``` + +그러나 `server` 외부에서는 사용해도 대부분 괜찮다. + +```typescript +// app/hooks/useEnv.tsx + +// ok +import { type ClientEnv, clientEnvSchema } from '@server/constants/env'; +``` + +## `remixCloudflareDevProxyVitePlugin` + +Cloudflare Pages는 일반적인 Node 환경이 아니기에, `wrangler`로 실행해야 실제 배포 환경에 맞게 실행해볼 수 있다. + +그러나 Remix.js는 `wrangler`를 사용하려면 먼저 빌드를 한 뒤 빌드 결과물을 실행해야 하기에, 개발 중 Live Reload가 동작하지 않는다. + +[`@remix-run/dev`의 `cloudflareDevProxyVitePlugin`을 사용하면](https://remix.run/docs/en/main/guides/vite#cloudflare-proxy) `wrangler`를 사용하지 않고도 Vite 개발 환경에서 Cloudflare Pages 환경에 맞게 쓰인 코드를 문제 없이 실행할 수 있지만, 현재 프로젝트 구성상 문제가 있다. + +우리 프로젝트는 `authSessionStorage`라는 cookie session storage를 생성한 뒤, `loader` 또는 `action`의 Response가 반환되기 직전에 `commitSession`을 실행해서 `Set-Cookie` header를 설정하도록 되어 있다. + +```typescript +// functions/[[path]].ts + +const handleRequest = createPagesFunctionHandler({ + build, + getLoadContext: (args) => getLoadContext(authSession, args), +}); + +const response = await handleRequest(context); + +response.headers.append( + 'Set-Cookie', + await authSessionStorage.commitSession(authSession), +); +``` + +그러나 `cloudflareDevProxyVitePlugin`은 `getLoadContext`만 parameter로 받고 있어 `Response`에 접근할 수 없기에, 모든 Remix.js의 endpoint에서 매번 `commitSession`을 수행해야만 한다. + +따라서 기존 플러그인 코드를 직접 수정하여 `commitSession`을 수행하도록 만들었다. + +```typescript +// server/plugins/remixCloudflareDevProxyVitePlugin.ts + +const res = await handler(req, loadContext); + +res.headers.append( + 'Set-Cookie', + await authSessionStorage.commitSession(authSession), +); +``` diff --git a/app/constants/api.ts b/server/constants/api.ts similarity index 100% rename from app/constants/api.ts rename to server/constants/api.ts diff --git a/server/constants/env.ts b/server/constants/env.ts new file mode 100644 index 0000000..225a688 --- /dev/null +++ b/server/constants/env.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +export const clientEnvSchema = z.object({ + KAKAO_CLIENT_ID: z.string(), +}); + +export const serverEnvSchema = z.object({ + AUTH_COOKIE_SESSION_SECRET: z.string(), + API_URL: z.string(), +}); + +export type ClientEnv = Readonly>; + +export type ServerEnv = Readonly>; + +export type CloudflareEnv = { + readonly KV_NAMESPACE: KVNamespace; +}; + +export type ContextEnv = ClientEnv & ServerEnv & CloudflareEnv; diff --git a/app/constants/types/api.ts b/server/constants/types/api.ts similarity index 100% rename from app/constants/types/api.ts rename to server/constants/types/api.ts diff --git a/app/constants/types/auth.ts b/server/constants/types/auth.ts similarity index 100% rename from app/constants/types/auth.ts rename to server/constants/types/auth.ts diff --git a/server/plugins/remixCloudflareDevProxyVitePlugin.ts b/server/plugins/remixCloudflareDevProxyVitePlugin.ts new file mode 100644 index 0000000..80e2ba7 --- /dev/null +++ b/server/plugins/remixCloudflareDevProxyVitePlugin.ts @@ -0,0 +1,193 @@ +/** + * `cloudflareDevProxyVitePlugin` 코드 원문을 가져와 수정 + * https://github.com/remix-run/remix/blob/main/packages/remix-dev/vite/cloudflare-proxy-plugin.ts + */ + +import { once } from 'node:events'; +import type { IncomingHttpHeaders, ServerResponse } from 'node:http'; +import { Readable } from 'node:stream'; +import { createRequestHandler, type ServerBuild } from '@remix-run/cloudflare'; +import { createReadableStreamFromReadable } from '@remix-run/node'; +import { splitCookiesString } from 'set-cookie-parser'; +import type * as Vite from 'vite'; +import { type GetPlatformProxyOptions } from 'wrangler'; +import type { ContextEnv } from '../constants/env'; +import { getLoadContext, makeAuthSession } from '../utils/cloudflare'; + +function invariant(value: boolean, message?: string): asserts value; + +function invariant( + value: T | null | undefined, + message?: string, +): asserts value is T; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function invariant(value: any, message?: string) { + if (value === false || value === null || typeof value === 'undefined') { + console.error( + 'The following error is a bug in Remix; please open an issue! https://github.com/remix-run/remix/issues/new', + ); + throw new Error(message); + } +} + +function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers { + const headers = new Headers(); + + for (const [key, values] of Object.entries(nodeHeaders)) { + if (values) { + if (Array.isArray(values)) { + for (const value of values) { + headers.append(key, value); + } + } else { + headers.set(key, values); + } + } + } + + return headers; +} + +// Based on `createRemixRequest` in packages/remix-express/server.ts +export function fromNodeRequest( + nodeReq: Vite.Connect.IncomingMessage, +): Request { + const origin = + nodeReq.headers.origin && 'null' !== nodeReq.headers.origin + ? nodeReq.headers.origin + : `http://${nodeReq.headers.host}`; + // Use `req.originalUrl` so Remix is aware of the full path + invariant( + nodeReq.originalUrl, + 'Expected `nodeReq.originalUrl` to be defined', + ); + const url = new URL(nodeReq.originalUrl, origin); + const init: RequestInit = { + method: nodeReq.method, + headers: fromNodeHeaders(nodeReq.headers), + }; + + if (nodeReq.method !== 'GET' && nodeReq.method !== 'HEAD') { + init.body = createReadableStreamFromReadable(nodeReq); + (init as { duplex: 'half' }).duplex = 'half'; + } + + return new Request(url.href, init); +} + +// Adapted from solid-start's `handleNodeResponse`: +// https://github.com/solidjs/solid-start/blob/7398163869b489cce503c167e284891cf51a6613/packages/start/node/fetch.js#L162-L185 +export async function toNodeRequest(res: Response, nodeRes: ServerResponse) { + nodeRes.statusCode = res.status; + nodeRes.statusMessage = res.statusText; + + const cookiesStrings = []; + + for (const [name, value] of res.headers) { + if (name === 'set-cookie') { + cookiesStrings.push(...splitCookiesString(value)); + } else nodeRes.setHeader(name, value); + } + + if (cookiesStrings.length) { + nodeRes.setHeader('set-cookie', cookiesStrings); + } + + if (res.body) { + // https://github.com/microsoft/TypeScript/issues/29867 + const responseBody = res.body as unknown as AsyncIterable; + const readable = Readable.from(responseBody); + readable.pipe(nodeRes); + await once(readable, 'end'); + } else { + nodeRes.end(); + } +} + +const serverBuildId = 'virtual:remix/server-build'; + +type CfProperties = Record; + +function importWrangler() { + try { + return import('wrangler'); + } catch (_) { + throw Error('Could not import `wrangler`. Do you have it installed?'); + } +} + +const NAME = 'vite-plugin-remix-cloudflare-proxy'; + +export const remixCloudflareDevProxyVitePlugin = ( + options: GetPlatformProxyOptions = {}, +): Vite.Plugin => ({ + name: NAME, + config: () => ({ + ssr: { + resolve: { + externalConditions: ['workerd', 'worker'], + }, + }, + }), + configResolved: (viteConfig) => { + const pluginIndex = (name: string) => + viteConfig.plugins.findIndex((plugin) => plugin.name === name); + const remixIndex = pluginIndex('remix'); + if (remixIndex >= 0 && remixIndex < pluginIndex(NAME)) { + throw new Error( + `The "${NAME}" plugin should be placed before the Remix plugin in your Vite config file`, + ); + } + }, + configureServer: async (viteDevServer) => { + const { getPlatformProxy } = await importWrangler(); + // Do not include `dispose` in Cloudflare context + const { dispose: _dispose, ...cloudflare } = await getPlatformProxy< + ContextEnv, + Cf + >(options); + const context = { cloudflare }; + return () => { + if (!viteDevServer.config.server.middlewareMode) { + viteDevServer.middlewares.use(async (nodeReq, nodeRes, next) => { + try { + const build = (await viteDevServer.ssrLoadModule( + serverBuildId, + )) as ServerBuild; + + const handler = createRequestHandler(build, 'development'); + const req = fromNodeRequest(nodeReq); + + // auth session 생성 + const { authSessionStorage, authSession } = await makeAuthSession( + { + authCookieSessionSecret: + context.cloudflare.env.AUTH_COOKIE_SESSION_SECRET, + kvNamespace: context.cloudflare.env.KV_NAMESPACE, + }, + nodeReq.headers.cookie, + ); + + const loadContext = await getLoadContext(authSession, { + request: req, + context, + }); + + const res = await handler(req, loadContext); + + // 자동 commit + res.headers.append( + 'Set-Cookie', + await authSessionStorage.commitSession(authSession), + ); + + await toNodeRequest(res, nodeRes); + } catch (error) { + next(error); + } + }); + } + }; + }, +}); diff --git a/app/utils/api.server.ts b/server/utils/api.ts similarity index 95% rename from app/utils/api.server.ts rename to server/utils/api.ts index 7fb8963..6a35712 100644 --- a/app/utils/api.server.ts +++ b/server/utils/api.ts @@ -1,7 +1,7 @@ -import { getAuthToken } from './auth.server'; -import { ApiError } from '@/constants/api'; -import type { ApiInfo } from '@/constants/types/api'; -import type { AuthSession } from '@/constants/types/auth'; +import { ApiError } from '../constants/api'; +import type { ApiInfo } from '../constants/types/api'; +import type { AuthSession } from '../constants/types/auth'; +import { getAuthToken } from './auth'; const COMMON_ERROR: { errorByStatus: Record< diff --git a/app/utils/auth.server.ts b/server/utils/auth.ts similarity index 97% rename from app/utils/auth.server.ts rename to server/utils/auth.ts index 2fcef13..fc48462 100644 --- a/app/utils/auth.server.ts +++ b/server/utils/auth.ts @@ -1,4 +1,4 @@ -import type { AuthSession, AuthSessionData } from '@/constants/types/auth'; +import type { AuthSession, AuthSessionData } from '../constants/types/auth'; export const clearAuthToken = async (authSession: AuthSession) => { authSession.unset('accessToken'); diff --git a/server/utils/cloudflare.ts b/server/utils/cloudflare.ts new file mode 100644 index 0000000..17fc747 --- /dev/null +++ b/server/utils/cloudflare.ts @@ -0,0 +1,69 @@ +import { + createCookie, + createWorkersKVSessionStorage, +} from '@remix-run/cloudflare'; +import { type GetLoadContextFunction } from '@remix-run/cloudflare-pages'; +import { type PlatformProxy } from 'wrangler'; +import { + type CloudflareEnv, + type ContextEnv, + type ClientEnv, + type ServerEnv, + clientEnvSchema, + serverEnvSchema, +} from '../constants/env'; +import type { FetchApi } from '../constants/types/api'; +import type { AuthSession, AuthSessionData } from '../constants/types/auth'; +import { fetchApi } from '../utils/api'; + +declare module '@remix-run/cloudflare' { + interface AppLoadContext extends ServerEnv, CloudflareEnv { + readonly clientEnv: ClientEnv; + readonly authSession: AuthSession; + readonly fetchApi: FetchApi; + } +} + +export const getLoadContext: ( + authSession: AuthSession, + args: { + request: Request; + context: { + cloudflare: Omit, 'dispose'>; + }; + }, +) => ReturnType> = async ( + authSession, + { context }, +) => { + const clientEnv = clientEnvSchema.parse(context.cloudflare.env); + serverEnvSchema.parse(context.cloudflare.env); + + return { + ...context.cloudflare.env, + clientEnv, + authSession, + fetchApi: async (apiInfo, variables) => + fetchApi(apiInfo, variables, context.cloudflare.env.API_URL, authSession), + }; +}; + +export const makeAuthSession = async ( + env: { + authCookieSessionSecret: string; + kvNamespace: KVNamespace; + }, + cookieHeader?: string | null, +) => { + const authSessionCookie = createCookie('__auth_session', { + secrets: [env.authCookieSessionSecret], + sameSite: true, + }); + const authSessionStorage = createWorkersKVSessionStorage({ + cookie: authSessionCookie, + kv: env.kvNamespace, + }); + const authSession = await authSessionStorage.getSession(cookieHeader); + + return { authSessionStorage, authSession }; +}; diff --git a/tsconfig.json b/tsconfig.json index fc3bed6..36cfdd6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,8 @@ "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { - "@/*": ["./app/*"] + "@/*": ["./app/*"], + "@server/*": ["./server/*"] }, // Vite takes care of building everything, not tsc. diff --git a/vite.config.ts b/vite.config.ts index 7e9818f..2050f25 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,12 @@ -import { - vitePlugin as remix, - cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, -} from '@remix-run/dev'; +import { vitePlugin as remix } from '@remix-run/dev'; import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; +import { remixCloudflareDevProxyVitePlugin } from './server/plugins/remixCloudflareDevProxyVitePlugin'; export default defineConfig({ plugins: [ - remixCloudflareDevProxy(), + remixCloudflareDevProxyVitePlugin(), remix({ future: { v3_fetcherPersist: true,