diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c8ec1fa4..b39ce2c9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,62 +13,486 @@ ### 기본과제 (Vanilla SSR & SSG) #### Express SSR 서버 -- [ ] Express 미들웨어 기반 서버 구현 -- [ ] 개발/프로덕션 환경 분기 처리 -- [ ] HTML 템플릿 치환 (``, ``) + +- [x] Express 미들웨어 기반 서버 구현 +- [x] 개발/프로덕션 환경 분기 처리 +- [x] HTML 템플릿 치환 (``, ``) #### 서버 사이드 렌더링 -- [ ] 서버에서 동작하는 Router 구현 -- [ ] 서버 데이터 프리페칭 (상품 목록, 상품 상세) -- [ ] 서버 상태관리 초기화 + +- [x] 서버에서 동작하는 Router 구현 +- [x] 서버 데이터 프리페칭 (상품 목록, 상품 상세) +- [x] 서버 상태관리 초기화 #### 클라이언트 Hydration -- [ ] `window.__INITIAL_DATA__` 스크립트 주입 -- [ ] 클라이언트 상태 복원 -- [ ] 서버-클라이언트 데이터 일치 + +- [x] `window.__INITIAL_DATA__` 스크립트 주입 +- [x] 클라이언트 상태 복원 +- [x] 서버-클라이언트 데이터 일치 #### Static Site Generation -- [ ] 동적 라우트 SSG (상품 상세 페이지들) -- [ ] 빌드 타임 페이지 생성 -- [ ] 파일 시스템 기반 배포 + +- [x] 동적 라우트 SSG (상품 상세 페이지들) +- [x] 빌드 타임 페이지 생성 +- [x] 파일 시스템 기반 배포 ### 심화과제 (React SSR & SSG) #### React SSR + - [ ] `renderToString` 서버 렌더링 - [ ] TypeScript SSR 모듈 빌드 - [ ] Universal React Router (서버/클라이언트 분기) - [ ] React 상태관리 서버 초기화 #### React Hydration + - [ ] Hydration 불일치 방지 - [ ] 클라이언트 상태 복원 #### Static Site Generation + - [ ] 동적 라우트 SSG (상품 상세 페이지들) - [ ] 빌드 타임 페이지 생성 - [ ] 파일 시스템 기반 배포 ## 아하! 모먼트 (A-ha! Moment) - +### AsyncLocalStorage를 통한 요청 격리의 중요성 + +처음에는 서버 렌더링에서 `globalThis`를 사용해 요청 컨텍스트를 저장했습니다. 그러다 동시 요청이 들어올 때 데이터가 섞이는 버그를 발견했고, Node.js의 `AsyncLocalStorage`를 알게 되었습니다. 이를 통해 각 요청이 독립적인 컨텍스트를 가질 수 있다는 것을 깨달았습니다. + +```javascript +// packages/vanilla/src/lib/asyncContext.js +await runWithContext(context, async () => { + const html = await render(route.component); + // 이 스코프 안에서 getContext()는 항상 올바른 요청의 context를 반환 +}); +``` + +### withLifecycle의 서버/클라이언트 분기 + +라이프사이클을 HOC 패턴으로 추상화하면서, 서버에서는 async 함수를, 클라이언트에서는 동기 함수를 반환해야 한다는 것을 알게 되었습니다. 서버는 데이터 프리페칭을 기다려야 하지만, 클라이언트는 즉시 렌더링 후 비동기로 업데이트하는 것이 UX에 더 좋기 때문입니다. ## 자유롭게 회고하기 - +### 구현하면서 집중한 부분들 + +#### 1. 요청별 컨텍스트 격리 + +서버 사이드 렌더링의 핵심은 동시 요청 처리입니다. 각 요청이 독립적인 상태를 유지해야 하므로 `AsyncLocalStorage`를 활용했습니다. + +```javascript +// packages/vanilla/src/lib/asyncContext.js +export const runWithContext = async (context, callback) => { + await initAsyncLocalStorage(); + return asyncLocalStorage.run(context, callback); +}; +``` + +#### 2. 라이프사이클 관리 + +기존 클라이언트 전용 라이프사이클을 서버에서도 동작하도록 확장했습니다. `withLifecycle` HOC를 통해: + +- 서버: `onMount`를 실행하고 데이터를 `initialData`에 저장 +- 클라이언트: `window.__INITIAL_DATA__`에서 초기 데이터 복원 후 필요시 재요청 + +#### 3. 메타태그 동적 생성 + +상품 상세 페이지의 경우, SEO를 위해 동적 메타태그가 필수입니다. `updateInitialData`를 통해 렌더링 중에 메타 정보를 수집하고, 서버에서 HTML에 주입하는 방식을 구현했습니다. + +```javascript +// packages/vanilla/src/pages/ProductDetailPage.js +updateInitialData("meta", { + title: `${product.title} - 쇼핑몰`, + description: `${product.title} - ${product.brand || "쇼핑몰"}`, + image: product.image, +}); +``` + +#### 4. Universal Code + +가능한 한 많은 코드를 서버/클라이언트에서 공유하도록 설계했습니다: + +- `Router`: 동일한 라우팅 로직 +- `withLifecycle`: 환경에 따라 다른 동작이지만 동일한 인터페이스 +- `createStorage`: 서버에서는 no-op, 클라이언트에서는 localStorage 사용 + +#### 5. Static Site Generation (SSG) + +**SSR 인프라 100% 재사용**: 기존 `render()`, `runWithContext()`, `withLifecycle` 등 모든 SSR 로직을 그대로 사용했습니다. 별도의 SSG 전용 코드를 작성하지 않고, 빌드 타임에 SSR을 실행하는 방식으로 구현했습니다. + +**Global fetch 폴리필**: MSW는 실제 네트워크 요청을 보내려고 해서 샌드박스 환경에서 실패했습니다. 대신 `globalThis.fetch`를 직접 폴리필해서 `items.json` 데이터를 반환하도록 구현했습니다. + +```javascript +// packages/vanilla/static-site-generate.js +globalThis.fetch = async (url) => { + const urlObj = new URL(url, "http://localhost"); + // /api/products, /api/products/:id, /api/categories 모두 처리 + return { ok: true, json: async () => mockData }; +}; +``` + +**340개 페이지 자동 생성**: `items.json`에서 상품 ID를 추출해 각 상품마다 `/product/:id/index.html` 생성. 홈페이지와 404 페이지까지 총 342개의 정적 HTML 파일이 생성됩니다. + +**SEO 최적화 완료**: 각 상품 페이지는 동적 메타태그가 포함되어 있고, 완전히 렌더링된 HTML을 제공하므로 검색 엔진 크롤러가 내용을 바로 인덱싱할 수 있습니다. + +### 아쉬운 부분들 + +#### 1. Store의 서버 격리 미흡 + +`productStore`, `cartStore` 등은 싱글톤으로 동작합니다. 서버 환경에서는 각 요청마다 독립적인 store 인스턴스가 필요할 수 있는데, 현재는 `initialData`를 통해 클라이언트에 전달하는 방식으로만 해결했습니다. 동시 요청이 많아지면 race condition 가능성이 있습니다. + +#### 2. 에러 바운더리 부재 + +서버 렌더링 중 에러 발생 시 적절한 fallback이 없습니다. 현재는 try-catch로 잡힌 에러만 처리하고 있어, 예상치 못한 에러가 발생하면 서버가 크래시할 수 있습니다. + +#### 3. 성능 최적화 여지 + +- HTML 템플릿이 매 요청마다 문자열로 생성됨 (캐싱 가능) +- CSS 파일을 매번 읽음 (메모리 캐싱 필요) +- 중복되는 HTML 템플릿 코드 (404와 일반 페이지) + +### 기술적 도전과 해결 + +#### 1. AsyncLocalStorage 동작 원리 이해하기 + +**문제 상황:** +초기에는 각 요청의 컨텍스트를 `globalThis`에 저장했는데, 동시에 2개의 요청이 들어오면 나중 요청이 먼저 요청의 데이터를 덮어쓰는 문제가 발생했습니다. + +```javascript +// 잘못된 접근 (초기 버전) +app.get("/product/:id", async (req, res) => { + globalThis.pathname = req.url; + globalThis.params = req.params; + const html = await render(); // 비동기 중에 다른 요청이 globalThis를 덮어쓸 수 있음 +}); +``` + +**해결 방법:** +`AsyncLocalStorage`는 비동기 콜백 체인 전체에서 격리된 스토리지를 제공합니다. `async_hooks` 모듈의 실행 컨텍스트 추적을 활용해, 같은 요청에서 파생된 모든 비동기 작업이 동일한 컨텍스트를 공유하도록 했습니다. + +```javascript +// packages/vanilla/src/lib/asyncContext.js +const { AsyncLocalStorage } = await import("node:async_hooks"); +const asyncLocalStorage = new AsyncLocalStorage(); + +// 각 요청마다 독립적인 컨텍스트 실행 +export const runWithContext = async (context, callback) => { + return asyncLocalStorage.run(context, callback); +}; + +// 어디서든 현재 요청의 컨텍스트 접근 가능 +export const getContext = () => { + return asyncLocalStorage?.getStore(); +}; +``` + +**핵심 포인트:** + +- `asyncLocalStorage.run()`으로 시작된 비동기 체인 내부의 모든 함수는 같은 store에 접근 +- `await`, `Promise`, `setTimeout` 등을 거쳐도 컨텍스트가 유지됨 +- 다른 요청의 실행 컨텍스트와 완전히 격리됨 + +#### 2. Universal Router - 하나의 코드, 두 가지 환경 + +**도전 과제:** +클라이언트의 `window.location`과 서버의 `req` 객체는 완전히 다른 API입니다. 하지만 라우팅 로직은 동일해야 합니다. + +**구현 전략:** +환경 감지 레이어를 통해 통일된 인터페이스를 제공했습니다. + +```javascript +// packages/vanilla/src/lib/Router.js +function getOrigin() { + if ("window" in globalThis) { + return window.location.origin; + } else { + const context = getContext(); // AsyncLocalStorage에서 가져옴 + return context.origin; + } +} + +function getPathname() { + if ("window" in globalThis) { + return window.location.pathname; + } else { + const context = getContext(); + return String(context.pathname); + } +} +``` + +이렇게 하면 Router 클래스의 다른 메서드들은 환경에 상관없이 `getPathname()`, `getOrigin()`만 호출하면 됩니다. + +**쿼리 파라미터 통합:** +Express는 `req.query`로 객체를 주지만, 클라이언트는 `location.search`로 문자열을 줍니다. `URLSearchParams`로 통일했습니다. + +```javascript +// packages/vanilla/src/lib/Router.js +get query() { + return Router.parseQuery(getSearch()); +} + +static parseQuery = (search) => { + const params = new URLSearchParams(search); + const query = {}; + for (const [key, value] of params) { + query[key] = value; + } + return query; +}; +``` + +서버에서는 `context.search` 객체를 받아서 다시 쿼리 문자열로 변환해 `URLSearchParams`에 넣으므로, 파싱 로직이 완전히 동일하게 동작합니다. + +#### 3. withLifecycle의 서버/클라이언트 이중 동작 + +**설계 결정:** +`withLifecycle`은 동일한 HOC지만, 반환하는 함수의 동작 방식이 환경에 따라 달라야 합니다. + +```javascript +// packages/vanilla/src/router/withLifecycle.js +export const withLifecycle = ({ onMount, onUnmount, watches } = {}, page) => { + const lifecycle = getPageLifecycle(page); + + // 라이프사이클 설정 + if (typeof onMount === "function") lifecycle.mount = onMount; + if (typeof onUnmount === "function") lifecycle.unmount = onUnmount; + if (Array.isArray(watches)) lifecycle.watches = watches; + + // 서버 환경: async 함수 반환 + if (isServer) { + return async (...args) => { + await mount(page); // 데이터 페칭 완료 대기 + return page(...args); // 렌더링 + }; + } + + // 클라이언트 환경: 동기 함수 반환 + return (...args) => { + const wasNewPage = pageState.current !== page; + + if (pageState.current && wasNewPage) { + unmount(pageState.current); // 이전 페이지 정리 + } + + pageState.current = page; + + if (wasNewPage) { + mount(page); // 비동기지만 await 안 함 (UX를 위해 즉시 렌더링) + } else { + // 같은 페이지 재렌더링 시 watches 체크 + lifecycle.watches?.forEach(([getDeps, callback], index) => { + const newDeps = getDeps(); + if (depsChanged(newDeps, lifecycle.deps[index])) { + callback(); + } + lifecycle.deps[index] = Array.isArray(newDeps) ? [...newDeps] : []; + }); + } + + return page(...args); + }; +}; +``` + +**왜 이렇게 분기했나:** + +- **서버**: SEO와 초기 로딩을 위해 완전히 렌더링된 HTML이 필요 → `await`로 데이터 로딩 완료를 기다림 +- **클라이언트**: 빠른 화면 전환을 위해 즉시 렌더링하고 데이터는 백그라운드에서 로드 → `await` 없이 비동기 실행 + +#### 4. initialData를 통한 Hydration + +**Hydration 불일치 방지:** +서버에서 렌더링한 HTML과 클라이언트의 첫 렌더링이 다르면 React Hydration Error와 비슷한 문제가 발생합니다. + +**해결 방법:** + +```javascript +// packages/vanilla/server.js +await runWithContext(context, async () => { + const html = await render(route.component); + + // 서버 렌더링 중 수집된 데이터를 스크립트로 주입 + res.send(` +
${html}
+ + `); +}); +``` + +클라이언트는 페이지 로드 시 `window.__INITIAL_DATA__`가 있으면 API 재호출을 건너뜁니다: + +```javascript +// packages/vanilla/src/services/productService.js +export const loadProductsAndCategories = async () => { + if ("window" in globalThis && productStore.getState().status === "done") { + return productStore.getState(); // 이미 서버에서 로드됨 + } + + // API 호출... +}; +``` + +**메타태그 동적 생성도 동일한 패턴:** + +```javascript +// packages/vanilla/src/pages/ProductDetailPage.js +updateInitialData("meta", { + title: `${product.title} - 쇼핑몰`, + description: product.description, + image: product.image, +}); +``` + +렌더링 중에 `initialData.meta`를 설정하면, 서버가 이를 읽어서 HTML ``에 주입합니다. + +#### 5. createStorage의 환경별 no-op 처리 + +**문제:** +`localStorage`는 브라우저에만 존재하므로, 서버에서 실행하면 에러가 발생합니다. + +**해결:** + +```javascript +// packages/vanilla/src/lib/createStorage.js +export const createStorage = (key, storage) => { + if (!("window" in globalThis)) { + // 서버 환경: no-op 반환 + return { + get: () => null, + set: () => {}, + reset: () => {}, + }; + } + + storage = storage ?? window.localStorage; + // 실제 로직... +}; +``` + +이렇게 하면 `cartStorage.get()` 같은 코드를 서버/클라이언트 양쪽에서 안전하게 호출할 수 있습니다. + +#### 6. SSG - Global fetch 폴리필로 네트워크 격리 + +**문제:** +MSW의 `setupServer`를 사용하려 했으나, 실제 네트워크 요청을 시도해서 샌드박스 환경에서 `EPERM` 에러가 발생했습니다. + +**해결:** +빌드 타임에는 네트워크가 필요 없습니다. `globalThis.fetch`를 직접 폴리필해서 `items.json` 데이터를 반환하도록 구현했습니다. + +```javascript +// packages/vanilla/static-site-generate.js +globalThis.fetch = async (url) => { + const urlObj = new URL(url, "http://localhost"); + const pathname = urlObj.pathname; + + // /api/products + if (pathname === "/api/products") { + const filtered = filterAndSortProducts(query); + return { ok: true, json: async () => ({ products: filtered, ... }) }; + } + + // /api/products/:id + const productMatch = pathname.match(/^\/api\/products\/(.+)$/); + if (productMatch) { + const product = items.find(item => item.productId === productMatch[1]); + return { ok: true, json: async () => product }; + } +}; +``` + +**왜 이 방법이 좋은가:** + +- MSW 같은 무거운 라이브러리 없이 순수 JavaScript로 해결 +- 네트워크 요청이 전혀 발생하지 않아 빌드 속도가 빠름 +- `items.json` 데이터를 직접 사용하므로 SSR handlers와 로직이 완전히 일치 +- 샌드박스 환경에서도 문제없이 동작 + +**SSG와 SSR 코드 100% 재사용:** + +```javascript +// 동일한 render 함수 사용 +import { render } from "./src/main-server.js"; +import { runWithContext } from "./src/lib/asyncContext.js"; + +// SSR과 똑같은 방식으로 페이지 렌더링 +await runWithContext(context, async () => { + const html = await render(route.component); +}); +``` + +SSG는 본질적으로 "빌드 타임에 실행하는 SSR"입니다. 기존 SSR 인프라를 전혀 수정하지 않고, fetch만 폴리필해서 340개 페이지를 자동 생성했습니다. + +### 배운 점 + +- **AsyncLocalStorage의 강력함**: 동시 요청을 격리하는 Node.js의 핵심 메커니즘. 이게 없으면 모든 상태를 요청 객체에 직접 전달해야 함 +- **Universal Code의 핵심은 추상화**: `window` 체크만으로 대부분의 환경 차이를 흡수할 수 있음 +- **서버는 동기적, 클라이언트는 비동기적**: 같은 기능도 UX와 SEO 요구사항에 따라 다른 실행 전략이 필요 +- **Hydration은 데이터 동기화**: 서버 렌더링 시점의 데이터를 클라이언트에 전달하는 게 핵심 +- **SSG = 빌드 타임 SSR**: SSG를 위해 새로운 코드를 작성할 필요 없음. SSR 로직을 빌드 타임에 실행하고 결과를 파일로 저장하면 됨 +- **프레임워크의 가치 재발견**: Next.js가 이 모든 것을 자동으로 처리해준다는 사실에 감사. 특히 `getStaticPaths`와 `getStaticProps`의 편리함을 실감 ## 리뷰 받고 싶은 내용 - +### 1. AsyncLocalStorage와 Store의 관계 + +현재 `AsyncLocalStorage`로 요청 컨텍스트(`origin`, `pathname`, `params`, `initialData`)는 격리했지만, `productStore` 자체는 여전히 전역 싱글톤입니다. + +```javascript +// packages/vanilla/src/stores/productStore.js +export const productStore = createStore(initialProductState, productReducer); +``` + +서버 렌더링 중에 `productStore.dispatch()`를 호출하는데, 동시에 두 요청이 다른 상품을 조회하면 store 상태가 섞일 수 있습니다. + +**질문:** + +- `productStore`도 `AsyncLocalStorage`에 넣어서 각 요청마다 독립적인 인스턴스를 만들어야 할까요? +- 아니면 서버에서는 store를 사용하지 않고, `initialData`에 직접 데이터를 담는 방식으로 리팩토링해야 할까요? +- Redux의 SSR 방식처럼 각 요청마다 `createStore()`를 새로 호출하는 게 정답일까요? + +현재는 다행히 렌더링이 빠르게 끝나서 실질적인 충돌이 없지만, 부하 테스트를 하면 문제가 발생할 것 같습니다. + +### 2. Router의 params 접근 방식 + +Router에서 `params`를 가져올 때 두 가지 방법을 혼용하고 있습니다: + +```javascript +// packages/vanilla/src/lib/Router.js +get params() { + if (this.#route?.params) { + return this.#route.params; // 클라이언트: 라우트 매칭 결과에서 + } + if ("window" in globalThis) { + return {}; + } + const context = getContext(); // 서버: AsyncLocalStorage에서 + return context.params ?? {}; +} +``` + +**문제점:** +서버에서는 Express의 `req.params`를 컨텍스트에 저장했지만, 클라이언트에서는 자체 정규식 매칭으로 추출합니다. 두 방식의 결과가 항상 일치한다고 보장할 수 있을까요? + +특히 인코딩 문제(`/product/한글` 같은 URL)나 특수문자가 있을 때 차이가 생길 수 있을 것 같습니다. 서버에서도 Express Router가 아닌 자체 정규식으로 통일해야 할까요? + +### 3. initialData의 Hydration 타이밍 + +현재 `window.__INITIAL_DATA__`를 확인하는 로직이 각 서비스에 흩어져 있습니다: + +```javascript +// packages/vanilla/src/services/productService.js +if ("window" in globalThis && productStore.getState().status === "done") { + return productStore.getState(); +} +``` + +**질문:** + +- `main.js`에서 앱 시작 시 `window.__INITIAL_DATA__`를 읽어서 모든 store를 한 번에 초기화하는 게 더 깔끔하지 않을까요? +- 현재 방식은 각 페이지가 마운트될 때마다 `status === "done"` 체크를 하는데, 이게 정말 안전할까요? +- `window.__INITIAL_DATA__`를 사용한 후에는 삭제해서 메모리를 해제하는 게 좋을까요? diff --git a/e2e/createTests.ts b/e2e/createTests.ts index 9b6246b7..b301a9fa 100644 --- a/e2e/createTests.ts +++ b/e2e/createTests.ts @@ -666,6 +666,7 @@ export const createSSRTest = (baseUrl: string) => { // SSR로 렌더링된 초기 HTML에 상품 목록이 포함되어야 함 const bodyContent = await page.locator("body").textContent(); + expect(bodyContent).toContain("총"); expect(bodyContent).toContain("개"); @@ -684,8 +685,12 @@ export const createSSRTest = (baseUrl: string) => { // HTML에 window.__INITIAL_DATA__ 스크립트가 포함되어 있는지 확인 expect(html).toContain( - `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}],"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}},"totalCount":340`, + `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}]`, + ); + expect(html).toContain( + `"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}}`, ); + expect(html).toContain(`"totalCount":340`); }); }); @@ -856,8 +861,12 @@ export const createSSGTest = (baseUrl: string) => { // HTML에 window.__INITIAL_DATA__ 스크립트가 포함되어 있는지 확인 expect(html).toContain( - `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}],"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}},"totalCount":340`, + `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}]`, + ); + expect(html).toContain( + `"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}}`, ); + expect(html).toContain(`"totalCount":340`); }); }); diff --git a/packages/vanilla/package.json b/packages/vanilla/package.json index ab5ae3fd..666e687e 100644 --- a/packages/vanilla/package.json +++ b/packages/vanilla/package.json @@ -38,6 +38,7 @@ "@testing-library/user-event": "^14.6.1", "@vitest/coverage-v8": "latest", "@vitest/ui": "^2.1.8", + "concurrently": "latest", "eslint": "^9.16.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", @@ -49,8 +50,7 @@ "msw": "^2.10.2", "prettier": "^3.4.2", "vite": "npm:rolldown-vite@latest", - "vitest": "latest", - "concurrently": "latest" + "vitest": "latest" }, "msw": { "workerDirectory": [ @@ -58,6 +58,7 @@ ] }, "dependencies": { + "@mswjs/http-middleware": "^0.10.3", "compression": "^1.8.1", "express": "^5.1.0", "sirv": "^3.0.1" diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js index 67f03afa..7cf15923 100644 --- a/packages/vanilla/server.js +++ b/packages/vanilla/server.js @@ -1,34 +1,172 @@ import express from "express"; +import fs from "fs/promises"; +import routes from "./src/routes.js"; +import { createMiddleware } from "@mswjs/http-middleware"; +import { handlers } from "./src/mocks/handlers.js"; +import { createServer as createViteServer } from "vite"; +import { render } from "./src/main-server.js"; +import { runWithContext } from "./src/lib/asyncContext.js"; + +const app = express(); const prod = process.env.NODE_ENV === "production"; -const port = process.env.PORT || 5173; +const port = Number(process.env.PORT) || 5173; const base = process.env.BASE || (prod ? "/front_7th_chapter4-1/vanilla/" : "/"); -const app = express(); +let vite; +let htmlTemplate = ""; + +if (!prod) { + // 개발 환경: Vite 미들웨어 사용 + vite = await createViteServer({ + server: { middlewareMode: true }, + appType: "custom", + base, + }); + app.use(vite.middlewares); +} else { + // 프로덕션 환경: 빌드된 정적 파일 서빙 + const distPath = "./dist/vanilla"; + app.use(base, express.static(distPath)); + + // 빌드된 index.html을 템플릿으로 로드 + htmlTemplate = await fs.readFile(`${distPath}/index.html`, "utf-8"); +} -const render = () => { - return `
안녕하세요
`; -}; +app.use(createMiddleware(...handlers)); +app.use(express.static("public")); -app.get("*all", (req, res) => { - res.send( - ` +const styles = fs.readFile("./src/styles.css", "utf-8"); + +// HTML 생성 헬퍼 함수 +async function generateHtml({ html, title, metaTags = "", initialData }) { + if (prod) { + // 프로덕션: 빌드된 템플릿 사용 + let result = htmlTemplate + .replace("Document", `${title}`) + .replace("", metaTags) + .replace("", html); + + // 직전에 initialData 주입 + result = result.replace( + "", + ` +`, + ); + + return result; + } else { + // 개발: 간단한 템플릿 (Vite가 /src/main.js 처리) + return ` - + - Vanilla Javascript SSR + ${title} + ${metaTags} + + - -
${render()}
+ +
${html}
+ + - - `.trim(), - ); +`.trim(); + } +} + +routes.forEach((route) => { + if (route.path === ".*") { + return app.get(async (req, res) => { + const origin = `${req.protocol}://${req.get("host")}`; + + // 요청별로 격리된 컨텍스트 생성 + const context = { + origin, + pathname: req.url, + params: req.params, + search: req.query, + initialData: {}, + }; + + await runWithContext(context, async () => { + // globalThis에도 설정 (하위 호환성) + globalThis.origin = context.origin; + globalThis.pathname = context.pathname; + globalThis.params = context.params; + globalThis.search = context.search; + globalThis.initialData = context.initialData; + + const html = await render(route.component); + + res.send( + await generateHtml({ + html, + title: "404 - Page Not Found", + metaTags: '', + initialData: context.initialData, + }), + ); + }); + }); + } + + app.get(route.path, async (req, res) => { + const origin = `${req.protocol}://${req.get("host")}`; + + // 요청별로 격리된 컨텍스트 생성 + const context = { + origin, + pathname: req.url, + params: req.params, + search: req.query, + initialData: {}, + }; + + await runWithContext(context, async () => { + // globalThis에도 설정 (하위 호환성) + globalThis.origin = context.origin; + globalThis.pathname = context.pathname; + globalThis.params = context.params; + globalThis.search = context.search; + globalThis.initialData = context.initialData; + + const html = await render(route.component); + + // 메타태그 생성 + let metaTags = ``; + let title = route.title; + + if (context.initialData.meta) { + const meta = context.initialData.meta; + title = meta.title; + metaTags = ` + + + + `; + } + + res.send( + await generateHtml({ + html, + title, + metaTags, + initialData: context.initialData, + }), + ); + }); + }); }); -// Start http server app.listen(port, () => { - console.log(`React Server started at http://localhost:${port}`); + console.log(`Example app listening on port ${port}`); }); diff --git a/packages/vanilla/src/api/productApi.js b/packages/vanilla/src/api/productApi.js index c2330fbe..d64ca55b 100644 --- a/packages/vanilla/src/api/productApi.js +++ b/packages/vanilla/src/api/productApi.js @@ -1,3 +1,5 @@ +import { getContext } from "../lib/asyncContext.js"; + export async function getProducts(params = {}) { const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params; const page = params.current ?? params.page ?? 1; @@ -11,17 +13,21 @@ export async function getProducts(params = {}) { sort, }); - const response = await fetch(`/api/products?${searchParams}`); + const context = getContext(); + + const response = await fetch(`${context.origin ?? ""}/api/products?${searchParams}`); return await response.json(); } export async function getProduct(productId) { - const response = await fetch(`/api/products/${productId}`); + const context = getContext(); + const response = await fetch(`${context.origin ?? ""}/api/products/${productId}`); return await response.json(); } export async function getCategories() { - const response = await fetch("/api/categories"); + const context = getContext(); + const response = await fetch(`${context.origin ?? ""}/api/categories`); return await response.json(); } diff --git a/packages/vanilla/src/components/CartModal.js b/packages/vanilla/src/components/CartModal.js index f9695180..bbb947bd 100644 --- a/packages/vanilla/src/components/CartModal.js +++ b/packages/vanilla/src/components/CartModal.js @@ -1,4 +1,4 @@ -import { CartItem } from "./CartItem"; +import { CartItem } from "./CartItem.js"; export function CartModal({ items = [], selectedAll = false, isOpen = false }) { if (!isOpen) { diff --git a/packages/vanilla/src/components/PageWrapper.js b/packages/vanilla/src/components/PageWrapper.js new file mode 100644 index 00000000..64be65d2 --- /dev/null +++ b/packages/vanilla/src/components/PageWrapper.js @@ -0,0 +1,46 @@ +import { cartStore, uiStore } from "../stores/index.js"; +import { CartModal, Footer, Toast } from "."; + +export const PageWrapper = ({ headerLeft, children }) => { + const cart = cartStore.getState(); + const { cartModal, toast } = uiStore.getState(); + const cartSize = cart.items.length; + + const cartCount = ` + + ${cartSize > 99 ? "99+" : cartSize} + + `; + + return ` +
+
+
+
+ ${headerLeft} +
+ + +
+
+
+
+ +
+ ${children} +
+ + ${CartModal({ ...cart, isOpen: cartModal.isOpen })} + + ${Toast(toast)} + + ${Footer()} +
+ `; +}; diff --git a/packages/vanilla/src/components/ProductList.js b/packages/vanilla/src/components/ProductList.js index e32e49b1..62b9558e 100644 --- a/packages/vanilla/src/components/ProductList.js +++ b/packages/vanilla/src/components/ProductList.js @@ -1,4 +1,4 @@ -import { ProductCard, ProductCardSkeleton } from "./ProductCard"; +import { ProductCard, ProductCardSkeleton } from "./ProductCard.js"; const loadingSkeleton = Array(6).fill(0).map(ProductCardSkeleton).join(""); diff --git a/packages/vanilla/src/components/index.js b/packages/vanilla/src/components/index.js index ef27b3d5..3e5ad533 100644 --- a/packages/vanilla/src/components/index.js +++ b/packages/vanilla/src/components/index.js @@ -1,8 +1,8 @@ -export * from "./ProductCard"; -export * from "./SearchBar"; -export * from "./ProductList"; -export * from "./CartItem"; -export * from "./CartModal"; -export * from "./Toast"; -export * from "./Logo"; -export * from "./Footer"; +export * from "./ProductCard.js"; +export * from "./SearchBar.js"; +export * from "./ProductList.js"; +export * from "./CartItem.js"; +export * from "./CartModal.js"; +export * from "./Toast.js"; +export * from "./Logo.js"; +export * from "./Footer.js"; diff --git a/packages/vanilla/src/constants.js b/packages/vanilla/src/constants.js index 01627e92..d26e8372 100644 --- a/packages/vanilla/src/constants.js +++ b/packages/vanilla/src/constants.js @@ -1 +1 @@ -export const BASE_URL = import.meta.env.PROD ? "/front_7th_chapter4-1/vanilla/" : "/"; +export const BASE_URL = import.meta.env?.PROD ? "/front_7th_chapter4-1/vanilla/" : "/"; diff --git a/packages/vanilla/src/events.js b/packages/vanilla/src/events.js index 4d66284f..e23fd391 100644 --- a/packages/vanilla/src/events.js +++ b/packages/vanilla/src/events.js @@ -1,5 +1,5 @@ -import { addEvent, isNearBottom } from "./utils"; -import { router } from "./router"; +import { addEvent, isNearBottom } from "./utils/index.js"; +import { router } from "./router/index.js"; import { addToCart, clearCart, @@ -15,8 +15,8 @@ import { setSort, toggleCartSelect, updateCartQuantity, -} from "./services"; -import { productStore, uiStore, UI_ACTIONS } from "./stores"; +} from "./services/index.js"; +import { productStore, uiStore, UI_ACTIONS } from "./stores/index.js"; /** * 상품 관련 이벤트 등록 diff --git a/packages/vanilla/src/lib/Router.js b/packages/vanilla/src/lib/Router.js index 2238a878..d24fe88d 100644 --- a/packages/vanilla/src/lib/Router.js +++ b/packages/vanilla/src/lib/Router.js @@ -2,6 +2,7 @@ * 간단한 SPA 라우터 */ import { createObserver } from "./createObserver.js"; +import { getContext } from "./asyncContext.js"; export class Router { #routes; @@ -14,10 +15,12 @@ export class Router { this.#route = null; this.#baseUrl = baseUrl.replace(/\/$/, ""); - window.addEventListener("popstate", () => { - this.#route = this.#findRoute(); - this.#observer.notify(); - }); + if ("window" in globalThis) { + window.addEventListener("popstate", () => { + this.#route = this.#findRoute(); + this.#observer.notify(); + }); + } } get baseUrl() { @@ -25,7 +28,7 @@ export class Router { } get query() { - return Router.parseQuery(window.location.search); + return Router.parseQuery(getSearch()); } set query(newQuery) { @@ -34,7 +37,14 @@ export class Router { } get params() { - return this.#route?.params ?? {}; + if (this.#route?.params) { + return this.#route.params; + } + if ("window" in globalThis) { + return {}; + } + const context = getContext(); + return context.params ?? {}; } get route() { @@ -73,8 +83,8 @@ export class Router { }); } - #findRoute(url = window.location.pathname) { - const { pathname } = new URL(url, window.location.origin); + #findRoute(url = getPathname()) { + const { pathname } = new URL(url, getOrigin()); for (const [routePath, route] of this.#routes) { const match = pathname.match(route.regex); if (match) { @@ -103,10 +113,10 @@ export class Router { // baseUrl이 없으면 자동으로 붙여줌 let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url); - const prevFullUrl = `${window.location.pathname}${window.location.search}`; + const prevFullUrl = `${getPathname()}${getSearch()}`; // 히스토리 업데이트 - if (prevFullUrl !== fullUrl) { + if ("window" in globalThis && prevFullUrl !== fullUrl) { window.history.pushState(null, "", fullUrl); } @@ -130,7 +140,9 @@ export class Router { * @param {string} search - location.search 또는 쿼리 문자열 * @returns {Object} 파싱된 쿼리 객체 */ - static parseQuery = (search = window.location.search) => { + static parseQuery = (search) => { + search = getSearch() ?? window.location.search; + const params = new URLSearchParams(search); const query = {}; for (const [key, value] of params) { @@ -166,6 +178,39 @@ export class Router { }); const queryString = Router.stringifyQuery(updatedQuery); - return `${baseUrl}${window.location.pathname.replace(baseUrl, "")}${queryString ? `?${queryString}` : ""}`; + return `${baseUrl}${getPathname().replace(baseUrl, "")}${queryString ? `?${queryString}` : ""}`; }; } + +function getOrigin() { + if ("window" in globalThis) { + return window.location.origin; + } else { + const context = getContext(); + return context.origin; + } +} + +function getPathname() { + if ("window" in globalThis) { + return window.location.pathname; + } else { + const context = getContext(); + return String(context.pathname); + } +} + +function getSearch() { + if ("window" in globalThis) { + return window.location.search; + } else { + const context = getContext(); + if (Object.keys(context.search).length > 0) { + return `?${Object.entries(context.search) + .map(([key, value]) => `${key}=${value}`) + .join("&")}`; + } else { + return ""; + } + } +} diff --git a/packages/vanilla/src/lib/asyncContext.js b/packages/vanilla/src/lib/asyncContext.js new file mode 100644 index 00000000..94c13fca --- /dev/null +++ b/packages/vanilla/src/lib/asyncContext.js @@ -0,0 +1,77 @@ +// 서버 환경인지 체크 +const isServer = typeof window === "undefined"; + +// AsyncLocalStorage 인스턴스 (서버에서만 사용) +let asyncLocalStorage = null; + +/** + * AsyncLocalStorage 초기화 (서버에서만) + */ +const initAsyncLocalStorage = async () => { + if (!isServer || asyncLocalStorage) return; + + try { + const { AsyncLocalStorage } = await import("node:async_hooks"); + asyncLocalStorage = new AsyncLocalStorage(); + } catch (e) { + console.warn("AsyncLocalStorage 초기화 실패:", e); + } +}; + +/** + * 현재 요청의 컨텍스트 가져오기 + */ +export const getContext = () => { + if (!isServer) { + return { + origin: window.location.origin, + pathname: window.location.pathname, + params: {}, + search: {}, + initialData: {}, + }; + } + + const store = asyncLocalStorage?.getStore(); + if (!store) { + // fallback to globalThis for compatibility + return { + origin: globalThis.origin || "", + pathname: globalThis.pathname || "", + params: globalThis.params || {}, + search: globalThis.search || {}, + initialData: globalThis.initialData || {}, + }; + } + + return store; +}; + +/** + * 새로운 요청 컨텍스트 생성 및 실행 + */ +export const runWithContext = async (context, callback) => { + if (!isServer) { + return callback(); + } + + // AsyncLocalStorage 초기화 (아직 안 됐으면) + await initAsyncLocalStorage(); + + if (!asyncLocalStorage) { + // AsyncLocalStorage를 사용할 수 없으면 globalThis 사용 + return callback(); + } + + return asyncLocalStorage.run(context, callback); +}; + +/** + * 현재 컨텍스트의 initialData 업데이트 + */ +export const updateInitialData = (key, value) => { + const context = getContext(); + if (context.initialData) { + context.initialData[key] = value; + } +}; diff --git a/packages/vanilla/src/lib/createStorage.js b/packages/vanilla/src/lib/createStorage.js index 08b504f2..94ca43cf 100644 --- a/packages/vanilla/src/lib/createStorage.js +++ b/packages/vanilla/src/lib/createStorage.js @@ -4,7 +4,17 @@ * @param {Storage} storage - 기본값은 localStorage * @returns {Object} { get, set, reset } */ -export const createStorage = (key, storage = window.localStorage) => { +export const createStorage = (key, storage) => { + if (!("window" in globalThis)) { + return { + get: () => null, + set: () => {}, + reset: () => {}, + }; + } + + storage = storage ?? window.localStorage; + const get = () => { try { const item = storage.getItem(key); diff --git a/packages/vanilla/src/lib/createStore.js b/packages/vanilla/src/lib/createStore.js index 19c74f82..19c4baf3 100644 --- a/packages/vanilla/src/lib/createStore.js +++ b/packages/vanilla/src/lib/createStore.js @@ -1,4 +1,5 @@ -import { createObserver } from "./createObserver"; +import { createObserver } from "./createObserver.js"; +import { updateInitialData } from "./asyncContext.js"; /** * Redux-style Store 생성 함수 @@ -6,15 +7,25 @@ import { createObserver } from "./createObserver"; * @param {*} initialState - 초기 상태 * @returns {Object} { getState, dispatch, subscribe } */ -export const createStore = (reducer, initialState) => { +export const createStore = (key, reducer, initialState) => { const { subscribe, notify } = createObserver(); + if ("window" in globalThis) { + initialState = window.__INITIAL_DATA__?.[key] ?? initialState; + } + let state = initialState; const getState = () => state; const dispatch = (action) => { const newState = reducer(state, action); + + // 서버 환경에서 컨텍스트에 상태 저장 + if (!("window" in globalThis)) { + updateInitialData(key, newState); + } + if (newState !== state) { state = newState; notify(); diff --git a/packages/vanilla/src/lib/index.js b/packages/vanilla/src/lib/index.js index a598ef30..1d3b3d28 100644 --- a/packages/vanilla/src/lib/index.js +++ b/packages/vanilla/src/lib/index.js @@ -1,4 +1,5 @@ -export * from "./createObserver"; -export * from "./createStore"; -export * from "./createStorage"; -export * from "./Router"; +export * from "./createObserver.js"; +export * from "./createStore.js"; +export * from "./createStorage.js"; +export * from "./Router.js"; +export * from "./asyncContext.js"; diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js index 40b58858..f8b39139 100644 --- a/packages/vanilla/src/main-server.js +++ b/packages/vanilla/src/main-server.js @@ -1,4 +1,3 @@ -export const render = async (url, query) => { - console.log({ url, query }); - return ""; +export const render = async (component) => { + return await component(); }; diff --git a/packages/vanilla/src/main.js b/packages/vanilla/src/main.js index 4c3f2765..cdb99abe 100644 --- a/packages/vanilla/src/main.js +++ b/packages/vanilla/src/main.js @@ -1,8 +1,8 @@ -import { registerGlobalEvents } from "./utils"; -import { initRender } from "./render"; -import { registerAllEvents } from "./events"; -import { loadCartFromStorage } from "./services"; -import { router } from "./router"; +import { registerGlobalEvents } from "./utils/index.js"; +import { initRender } from "./render.js"; +import { registerAllEvents } from "./events.js"; +import { loadCartFromStorage } from "./services/index.js"; +import { router } from "./router/index.js"; import { BASE_URL } from "./constants.js"; const enableMocking = () => @@ -23,7 +23,7 @@ function main() { router.start(); } -if (import.meta.env.MODE !== "test") { +if (import.meta.env?.MODE !== "test") { enableMocking().then(main); } else { main(); diff --git a/packages/vanilla/src/mocks/browser.js b/packages/vanilla/src/mocks/browser.js index be3dedca..e4d86a51 100644 --- a/packages/vanilla/src/mocks/browser.js +++ b/packages/vanilla/src/mocks/browser.js @@ -1,5 +1,5 @@ import { setupWorker } from "msw/browser"; -import { handlers } from "./handlers"; +import { handlers } from "./handlers.js"; // MSW 워커 설정 export const worker = setupWorker(...handlers); diff --git a/packages/vanilla/src/mocks/handlers.js b/packages/vanilla/src/mocks/handlers.js index 6e3035e6..9836e0f2 100644 --- a/packages/vanilla/src/mocks/handlers.js +++ b/packages/vanilla/src/mocks/handlers.js @@ -1,5 +1,5 @@ import { http, HttpResponse } from "msw"; -import items from "./items.json"; +import items from "./items.json" with { type: "json" }; const delay = async () => await new Promise((resolve) => setTimeout(resolve, 200)); diff --git a/packages/vanilla/src/pages/HomePage.js b/packages/vanilla/src/pages/HomePage.js index ca08c26c..52d4201f 100644 --- a/packages/vanilla/src/pages/HomePage.js +++ b/packages/vanilla/src/pages/HomePage.js @@ -1,13 +1,14 @@ -import { ProductList, SearchBar } from "../components"; -import { productStore } from "../stores"; -import { router, withLifecycle } from "../router"; -import { loadProducts, loadProductsAndCategories } from "../services"; +import { ProductList, SearchBar } from "../components/index.js"; +import { productStore } from "../stores/index.js"; +import { router, withLifecycle } from "../router/index.js"; +import { loadProducts, loadProductsAndCategories } from "../services/index.js"; import { PageWrapper } from "./PageWrapper.js"; export const HomePage = withLifecycle( { onMount: () => { - loadProductsAndCategories(); + console.log("onMount"); + return loadProductsAndCategories(); }, watches: [ () => { @@ -24,6 +25,8 @@ export const HomePage = withLifecycle( const category = { category1, category2 }; const hasMore = products.length < totalCount; + console.log(loading, totalCount); + return PageWrapper({ headerLeft: `

diff --git a/packages/vanilla/src/pages/NotFoundPage.js b/packages/vanilla/src/pages/NotFoundPage.js index be69ed5f..f4a82a0d 100644 --- a/packages/vanilla/src/pages/NotFoundPage.js +++ b/packages/vanilla/src/pages/NotFoundPage.js @@ -1,5 +1,5 @@ -import { PageWrapper } from "./PageWrapper"; -import { Logo } from "../components"; +import { PageWrapper } from "./PageWrapper.js"; +import { Logo } from "../components/index.js"; export const NotFoundPage = () => PageWrapper({ diff --git a/packages/vanilla/src/pages/PageWrapper.js b/packages/vanilla/src/pages/PageWrapper.js index fc13328e..f1527b63 100644 --- a/packages/vanilla/src/pages/PageWrapper.js +++ b/packages/vanilla/src/pages/PageWrapper.js @@ -1,5 +1,5 @@ -import { cartStore, uiStore } from "../stores"; -import { CartModal, Footer, Toast } from "../components"; +import { cartStore, uiStore } from "../stores/index.js"; +import { CartModal, Footer, Toast } from "../components/index.js"; export const PageWrapper = ({ headerLeft, children }) => { const cart = cartStore.getState(); diff --git a/packages/vanilla/src/pages/ProductDetailPage.js b/packages/vanilla/src/pages/ProductDetailPage.js index 73d0ec30..9cb926a6 100644 --- a/packages/vanilla/src/pages/ProductDetailPage.js +++ b/packages/vanilla/src/pages/ProductDetailPage.js @@ -1,7 +1,8 @@ -import { productStore } from "../stores"; -import { loadProductDetailForPage } from "../services"; -import { router, withLifecycle } from "../router"; +import { productStore } from "../stores/index.js"; +import { loadProductDetailForPage } from "../services/index.js"; +import { router, withLifecycle } from "../router/index.js"; import { PageWrapper } from "./PageWrapper.js"; +import { updateInitialData } from "../lib/asyncContext.js"; const loadingContent = `
@@ -237,13 +238,19 @@ function ProductDetail({ product, relatedProducts = [] }) { export const ProductDetailPage = withLifecycle( { onMount: () => { - loadProductDetailForPage(router.params.id); + return loadProductDetailForPage(router.params.id); }, watches: [() => [router.params.id], () => loadProductDetailForPage(router.params.id)], }, () => { const { currentProduct: product, relatedProducts = [], error, loading } = productStore.getState(); + updateInitialData("meta", { + title: `${product.title} - 쇼핑몰`, + description: `${product.title} - ${product.brand || "쇼핑몰"}`, + image: product.image, + }); + return PageWrapper({ headerLeft: `
diff --git a/packages/vanilla/src/pages/index.js b/packages/vanilla/src/pages/index.js index 1bf01f33..bb674eec 100644 --- a/packages/vanilla/src/pages/index.js +++ b/packages/vanilla/src/pages/index.js @@ -1,3 +1,3 @@ -export * from "./HomePage"; -export * from "./ProductDetailPage"; -export * from "./NotFoundPage"; +export * from "./HomePage.js"; +export * from "./ProductDetailPage.js"; +export * from "./NotFoundPage.js"; diff --git a/packages/vanilla/src/render.js b/packages/vanilla/src/render.js index 87f30f19..a0a913ba 100644 --- a/packages/vanilla/src/render.js +++ b/packages/vanilla/src/render.js @@ -1,18 +1,18 @@ -import { cartStore, productStore, uiStore } from "./stores"; -import { router } from "./router"; -import { HomePage, NotFoundPage, ProductDetailPage } from "./pages"; -import { withBatch } from "./utils"; +import { cartStore, productStore, uiStore } from "./stores/index.js"; +import { router } from "./router/index.js"; +import { withBatch } from "./utils/index.js"; +import routes from "./routes.js"; -// 홈 페이지 (상품 목록) -router.addRoute("/", HomePage); -router.addRoute("/product/:id/", ProductDetailPage); -router.addRoute(".*", NotFoundPage); +routes.forEach((route) => { + router.addRoute(route.path, route.component); +}); /** * 전체 애플리케이션 렌더링 */ export const render = withBatch(() => { const rootElement = document.getElementById("root"); + if (!rootElement) return; const PageComponent = router.target; diff --git a/packages/vanilla/src/router/index.js b/packages/vanilla/src/router/index.js index f4964f8d..4d84d2cb 100644 --- a/packages/vanilla/src/router/index.js +++ b/packages/vanilla/src/router/index.js @@ -1,2 +1,2 @@ -export * from "./router"; +export * from "./router.js"; export * from "./withLifecycle.js"; diff --git a/packages/vanilla/src/router/router.js b/packages/vanilla/src/router/router.js index d897ee76..d13383ed 100644 --- a/packages/vanilla/src/router/router.js +++ b/packages/vanilla/src/router/router.js @@ -1,5 +1,5 @@ // 글로벌 라우터 인스턴스 -import { Router } from "../lib"; +import { Router } from "../lib/index.js"; import { BASE_URL } from "../constants.js"; export const router = new Router(BASE_URL); diff --git a/packages/vanilla/src/router/withLifecycle.js b/packages/vanilla/src/router/withLifecycle.js index ccb21113..48e7785b 100644 --- a/packages/vanilla/src/router/withLifecycle.js +++ b/packages/vanilla/src/router/withLifecycle.js @@ -2,6 +2,9 @@ const lifeCycles = new WeakMap(); const pageState = { current: null, previous: null }; const initLifecycle = { mount: null, unmount: null, watches: [], deps: [], mounted: false }; +// 서버 환경 체크 +const isServer = typeof window === "undefined"; + // 페이지의 생명주기 상태를 가져오거나 초기화 const getPageLifecycle = (page) => { if (!lifeCycles.has(page)) { @@ -26,13 +29,15 @@ const depsChanged = (newDeps, oldDeps) => { return newDeps.some((dep, index) => dep !== oldDeps[index]); }; -// 페이지 마운트 처리 -const mount = (page) => { +// 페이지 마운트 처리 (async 지원) +const mount = async (page) => { const lifecycle = getPageLifecycle(page); - if (lifecycle.mounted) return; + if (!isServer && lifecycle.mounted) return; - // 마운트 콜백들 실행 - lifecycle.mount?.(); + // 마운트 콜백들 실행 (async일 수 있음) + if (lifecycle.mount) { + await lifecycle.mount(); + } lifecycle.mounted = true; lifecycle.deps = []; }; @@ -62,6 +67,18 @@ export const withLifecycle = ({ onMount, onUnmount, watches } = {}, page) => { lifecycle.watches = typeof watches[0] === "function" ? [watches] : watches; } + // 서버 환경에서는 async 함수 반환 + if (isServer) { + return async (...args) => { + // 서버에서는 항상 onMount 실행 후 렌더링 + await mount(page); + + // 페이지 함수 실행 + return page(...args); + }; + } + + // 클라이언트 환경에서는 동기 함수 (기존 동작) return (...args) => { const wasNewPage = pageState.current !== page; diff --git a/packages/vanilla/src/routes.js b/packages/vanilla/src/routes.js new file mode 100644 index 00000000..c7012836 --- /dev/null +++ b/packages/vanilla/src/routes.js @@ -0,0 +1,19 @@ +import { HomePage, NotFoundPage, ProductDetailPage } from "./pages/index.js"; + +export default [ + { + title: "쇼핑몰 - 홈", + path: "/", + component: HomePage, + }, + { + title: "쇼핑몰 - 상품 상세", + path: "/product/:id/", + component: ProductDetailPage, + }, + { + title: "404", + path: ".*", + component: NotFoundPage, + }, +]; diff --git a/packages/vanilla/src/services/cartService.js b/packages/vanilla/src/services/cartService.js index 85f7c5e9..d7adb1bc 100644 --- a/packages/vanilla/src/services/cartService.js +++ b/packages/vanilla/src/services/cartService.js @@ -1,5 +1,5 @@ -import { CART_ACTIONS, cartStore, UI_ACTIONS, uiStore } from "../stores"; -import { cartStorage } from "../storage"; +import { CART_ACTIONS, cartStore, UI_ACTIONS, uiStore } from "../stores/index.js"; +import { cartStorage } from "../storage/index.js"; /** * 로컬스토리지에서 장바구니 데이터 로드 diff --git a/packages/vanilla/src/services/index.js b/packages/vanilla/src/services/index.js index 845d25b4..782661e1 100644 --- a/packages/vanilla/src/services/index.js +++ b/packages/vanilla/src/services/index.js @@ -1,2 +1,2 @@ -export * from "./productService"; -export * from "./cartService"; +export * from "./productService.js"; +export * from "./cartService.js"; diff --git a/packages/vanilla/src/services/productService.js b/packages/vanilla/src/services/productService.js index 8a12e8bd..0f7c0f9d 100644 --- a/packages/vanilla/src/services/productService.js +++ b/packages/vanilla/src/services/productService.js @@ -1,8 +1,13 @@ -import { getCategories, getProduct, getProducts } from "../api/productApi"; -import { initialProductState, productStore, PRODUCT_ACTIONS } from "../stores"; -import { router } from "../router"; +import { getCategories, getProduct, getProducts } from "../api/productApi.js"; +import { initialProductState, productStore, PRODUCT_ACTIONS } from "../stores/index.js"; +import { router } from "../router/index.js"; export const loadProductsAndCategories = async () => { + console.log("loadProductsAndCategories", productStore.getState().status); + if ("window" in globalThis && productStore.getState().status === "done") { + return productStore.getState(); + } + router.query = { current: undefined }; // 항상 첫 페이지로 초기화 productStore.dispatch({ type: PRODUCT_ACTIONS.SETUP, diff --git a/packages/vanilla/src/storage/cartStorage.js b/packages/vanilla/src/storage/cartStorage.js index 7aa68383..acf9aee6 100644 --- a/packages/vanilla/src/storage/cartStorage.js +++ b/packages/vanilla/src/storage/cartStorage.js @@ -1,3 +1,3 @@ -import { createStorage } from "../lib"; +import { createStorage } from "../lib/index.js"; export const cartStorage = createStorage("shopping_cart"); diff --git a/packages/vanilla/src/storage/index.js b/packages/vanilla/src/storage/index.js index 122983be..27d82b35 100644 --- a/packages/vanilla/src/storage/index.js +++ b/packages/vanilla/src/storage/index.js @@ -1 +1 @@ -export * from "./cartStorage"; +export * from "./cartStorage.js"; diff --git a/packages/vanilla/src/stores/cartStore.js b/packages/vanilla/src/stores/cartStore.js index fe61f167..5a8f8c2f 100644 --- a/packages/vanilla/src/stores/cartStore.js +++ b/packages/vanilla/src/stores/cartStore.js @@ -1,5 +1,5 @@ -import { createStore } from "../lib"; -import { CART_ACTIONS } from "./actionTypes"; +import { createStore } from "../lib/index.js"; +import { CART_ACTIONS } from "./actionTypes.js"; import { cartStorage } from "../storage/index.js"; /** @@ -157,4 +157,4 @@ const cartReducer = (_, action) => { /** * 장바구니 스토어 생성 */ -export const cartStore = createStore(cartReducer, initialState); +export const cartStore = createStore("cart", cartReducer, initialState); diff --git a/packages/vanilla/src/stores/index.js b/packages/vanilla/src/stores/index.js index 36fefd54..2e2c7dda 100644 --- a/packages/vanilla/src/stores/index.js +++ b/packages/vanilla/src/stores/index.js @@ -1,4 +1,4 @@ -export * from "./actionTypes"; -export * from "./productStore"; -export * from "./cartStore"; -export * from "./uiStore"; +export * from "./actionTypes.js"; +export * from "./productStore.js"; +export * from "./cartStore.js"; +export * from "./uiStore.js"; diff --git a/packages/vanilla/src/stores/productStore.js b/packages/vanilla/src/stores/productStore.js index 0f39343d..509d8586 100644 --- a/packages/vanilla/src/stores/productStore.js +++ b/packages/vanilla/src/stores/productStore.js @@ -1,5 +1,5 @@ -import { createStore } from "../lib"; -import { PRODUCT_ACTIONS } from "./actionTypes"; +import { createStore } from "../lib/index.js"; +import { PRODUCT_ACTIONS } from "./actionTypes.js"; /** * 상품 스토어 초기 상태 @@ -103,4 +103,4 @@ const productReducer = (state, action) => { /** * 상품 스토어 생성 */ -export const productStore = createStore(productReducer, initialProductState); +export const productStore = createStore("product", productReducer, initialProductState); diff --git a/packages/vanilla/src/stores/uiStore.js b/packages/vanilla/src/stores/uiStore.js index 606603d7..3cc6a967 100644 --- a/packages/vanilla/src/stores/uiStore.js +++ b/packages/vanilla/src/stores/uiStore.js @@ -1,5 +1,5 @@ -import { createStore } from "../lib"; -import { UI_ACTIONS } from "./actionTypes"; +import { createStore } from "../lib/index.js"; +import { UI_ACTIONS } from "./actionTypes.js"; /** * UI 스토어 초기 상태 @@ -62,4 +62,4 @@ const uiReducer = (state, action) => { /** * UI 스토어 생성 */ -export const uiStore = createStore(uiReducer, initialState); +export const uiStore = createStore("ui", uiReducer, initialState); diff --git a/packages/vanilla/src/utils/index.js b/packages/vanilla/src/utils/index.js index b0495013..29167ecb 100644 --- a/packages/vanilla/src/utils/index.js +++ b/packages/vanilla/src/utils/index.js @@ -1,3 +1,3 @@ -export * from "./eventUtils"; -export * from "./domUtils"; -export * from "./withBatch"; +export * from "./eventUtils.js"; +export * from "./domUtils.js"; +export * from "./withBatch.js"; diff --git a/packages/vanilla/static-site-generate.js b/packages/vanilla/static-site-generate.js index c479f112..c96d63a4 100644 --- a/packages/vanilla/static-site-generate.js +++ b/packages/vanilla/static-site-generate.js @@ -1,20 +1,301 @@ import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { render } from "./src/main-server.js"; +import { runWithContext } from "./src/lib/asyncContext.js"; +import routes from "./src/routes.js"; +import items from "./src/mocks/items.json" with { type: "json" }; -const render = () => { - return `
안녕하세요
`; -}; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +// 카테고리 추출 (handlers.js와 동일) +function getUniqueCategories() { + const categories = {}; + items.forEach((item) => { + const cat1 = item.category1; + const cat2 = item.category2; + if (!categories[cat1]) categories[cat1] = {}; + if (cat2 && !categories[cat1][cat2]) categories[cat1][cat2] = {}; + }); + return categories; +} + +// 상품 필터링 및 정렬 (handlers.js와 동일) +function filterAndSortProducts(query) { + let filtered = [...items]; + + if (query.search) { + const searchTerm = query.search.toLowerCase(); + filtered = filtered.filter( + (item) => item.title.toLowerCase().includes(searchTerm) || item.brand.toLowerCase().includes(searchTerm), + ); + } + + if (query.category1) { + filtered = filtered.filter((item) => item.category1 === query.category1); + } + if (query.category2) { + filtered = filtered.filter((item) => item.category2 === query.category2); + } + + if (query.sort) { + switch (query.sort) { + case "price_asc": + filtered.sort((a, b) => parseInt(a.lprice) - parseInt(b.lprice)); + break; + case "price_desc": + filtered.sort((a, b) => parseInt(b.lprice) - parseInt(a.lprice)); + break; + case "name_asc": + filtered.sort((a, b) => a.title.localeCompare(b.title, "ko")); + break; + case "name_desc": + filtered.sort((a, b) => b.title.localeCompare(a.title, "ko")); + break; + default: + filtered.sort((a, b) => parseInt(a.lprice) - parseInt(b.lprice)); + } + } + + return filtered; +} + +// Global fetch 폴리필 (SSG 전용 - 로컬 데이터 반환) +function setupFetchPolyfill() { + globalThis.fetch = async (url) => { + const urlObj = new URL(url, "http://localhost"); + const pathname = urlObj.pathname; + const searchParams = urlObj.searchParams; + + // /api/products + if (pathname === "/api/products") { + const page = parseInt(searchParams.get("page") || searchParams.get("current") || "1"); + const limit = parseInt(searchParams.get("limit") || "20"); + const search = searchParams.get("search") || ""; + const category1 = searchParams.get("category1") || ""; + const category2 = searchParams.get("category2") || ""; + const sort = searchParams.get("sort") || "price_asc"; + + const filtered = filterAndSortProducts({ search, category1, category2, sort }); + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginated = filtered.slice(startIndex, endIndex); + + return { + ok: true, + json: async () => ({ + products: paginated, + pagination: { + page, + limit, + total: filtered.length, + totalPages: Math.ceil(filtered.length / limit), + hasNext: endIndex < filtered.length, + hasPrev: page > 1, + }, + filters: { search, category1, category2, sort }, + }), + }; + } + + // /api/products/:id + const productMatch = pathname.match(/^\/api\/products\/(.+)$/); + if (productMatch) { + const productId = productMatch[1]; + const product = items.find((item) => item.productId === productId); + + if (!product) { + return { + ok: false, + status: 404, + json: async () => ({ error: "Product not found" }), + }; + } + + return { + ok: true, + json: async () => ({ + ...product, + description: `${product.title}에 대한 상세 설명입니다. ${product.brand} 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.`, + rating: 4, + reviewCount: 100, + stock: 50, + images: [product.image], + }), + }; + } + + // /api/categories + if (pathname === "/api/categories") { + return { + ok: true, + json: async () => getUniqueCategories(), + }; + } + + throw new Error(`Unhandled fetch: ${url}`); + }; +} + +// 프로덕션 설정 +const DIST_DIR = path.resolve(__dirname, "../../dist/vanilla"); +const ORIGIN = "http://localhost:3000"; // SSG 빌드 시 임시 origin + +// HTML 템플릿 로드 (Vite 빌드 결과 사용) +let htmlTemplate = null; + +function loadHtmlTemplate() { + if (!htmlTemplate) { + const templatePath = path.resolve(DIST_DIR, "index.html"); + htmlTemplate = fs.readFileSync(templatePath, "utf-8"); + } + return htmlTemplate; +} + +// HTML 템플릿 생성 함수 +function createHtmlTemplate({ html, title, metaTags, initialData }) { + const template = loadHtmlTemplate(); + + // Vite 빌드된 템플릿에서 필요한 부분만 치환 + let result = template + .replace("Document", `${title}`) + .replace("", metaTags) + .replace("", html); + + // 태그 직전에 initialData 스크립트 주입 + result = result.replace( + "", + ` +`, + ); + + return result; +} + +// 페이지 렌더링 함수 +async function renderPage(route, params = {}, query = {}) { + const context = { + origin: ORIGIN, + pathname: route.path.replace(/:(\w+)/g, (_, key) => params[key] || ""), + params, + search: query, + initialData: {}, + }; + + let html = ""; + await runWithContext(context, async () => { + // globalThis에도 설정 (하위 호환성) + globalThis.origin = context.origin; + globalThis.pathname = context.pathname; + globalThis.params = context.params; + globalThis.search = context.search; + globalThis.initialData = context.initialData; + + html = await render(route.component); + }); + + // 메타태그 생성 + let metaTags = ``; + let title = route.title; + + if (context.initialData.meta) { + const meta = context.initialData.meta; + title = meta.title; + metaTags = ` + + + + `; + } + + return createHtmlTemplate({ + html, + title, + metaTags, + initialData: context.initialData, + }); +} + +// 파일 저장 함수 +function saveHtmlFile(filePath, content) { + const fullPath = path.join(DIST_DIR, filePath); + const dir = path.dirname(fullPath); + + // 디렉토리 생성 + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(fullPath, content, "utf-8"); + console.log(`✅ Generated: ${filePath}`); +} + +// SSG 메인 함수 async function generateStaticSite() { - // HTML 템플릿 읽기 - const template = fs.readFileSync("../../dist/vanilla/index.html", "utf-8"); + console.log("🚀 Starting Static Site Generation...\n"); + + // Global fetch 폴리필 설정 + setupFetchPolyfill(); + console.log("📡 Fetch polyfill configured for local data\n"); + + try { + // 1. 홈페이지 생성 + console.log("📄 Generating home page..."); + const homeRoute = routes.find((r) => r.path === "/"); + if (homeRoute) { + const homeHtml = await renderPage(homeRoute); + saveHtmlFile("index.html", homeHtml); + } + + // 2. 모든 상품 상세 페이지 생성 + console.log("\n📦 Generating product detail pages..."); + const productRoute = routes.find((r) => r.path === "/product/:id/"); + + if (productRoute) { + const productIds = items.map((item) => item.productId); + console.log(` Found ${productIds.length} products\n`); + + let successCount = 0; + let failCount = 0; + + for (const productId of productIds) { + try { + const productHtml = await renderPage(productRoute, { id: productId }); + saveHtmlFile(`product/${productId}/index.html`, productHtml); + successCount++; + } catch (error) { + console.error(`❌ Failed to generate product ${productId}:`, error.message); + failCount++; + } + } + + console.log(`\n ✅ Success: ${successCount} pages`); + if (failCount > 0) { + console.log(` ❌ Failed: ${failCount} pages`); + } + } - // 어플리케이션 렌더링하기 - const appHtml = render(); + // 3. 404 페이지 생성 + console.log("\n🚫 Generating 404 page..."); + const notFoundRoute = routes.find((r) => r.path === ".*"); + if (notFoundRoute) { + const notFoundHtml = await renderPage(notFoundRoute); + saveHtmlFile("404.html", notFoundHtml); + } - // 결과 HTML 생성하기 - const result = template.replace("", appHtml); - fs.writeFileSync("../../dist/vanilla/index.html", result); + console.log("\n✨ Static Site Generation completed successfully!"); + console.log(`📁 Output directory: ${DIST_DIR}\n`); + } catch (error) { + console.error("\n❌ SSG failed:", error); + throw error; + } } // 실행 -generateStaticSite(); +generateStaticSite().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 697cd2f9..a7ed676d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,22 +10,22 @@ importers: devDependencies: '@babel/core': specifier: latest - version: 7.28.3 + version: 7.28.5 '@babel/plugin-transform-react-jsx': specifier: latest - version: 7.27.1(@babel/core@7.28.3) + version: 7.27.1(@babel/core@7.28.5) '@eslint/js': specifier: ^9.16.0 version: 9.30.1 '@playwright/test': specifier: latest - version: 1.55.0 + version: 1.57.0 '@testing-library/dom': specifier: latest version: 10.4.1 '@testing-library/jest-dom': specifier: latest - version: 6.8.0 + version: 6.9.1 '@testing-library/user-event': specifier: latest version: 14.6.1(@testing-library/dom@10.4.1) @@ -34,10 +34,10 @@ importers: version: 24.0.13 '@vitest/coverage-v8': specifier: latest - version: 3.2.4(vitest@3.2.4) + version: 4.0.16(vitest@4.0.16) '@vitest/ui': specifier: latest - version: 3.2.4(vitest@3.2.4) + version: 4.0.16(vitest@4.0.16) concurrently: specifier: latest version: 9.2.1 @@ -73,38 +73,38 @@ importers: version: 8.36.0(eslint@9.30.1)(typescript@5.8.3) vite: specifier: npm:rolldown-vite@latest - version: rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0) + version: rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0) vitest: specifier: latest - version: 3.2.4(@types/node@24.0.13)(@vitest/ui@3.2.4)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) + version: 4.0.16(@types/node@24.0.13)(@vitest/ui@4.0.16)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) packages/lib: dependencies: react: specifier: latest - version: 19.1.1 + version: 19.2.3 react-dom: specifier: latest - version: 19.1.1(react@19.1.1) + version: 19.2.3(react@19.2.3) use-sync-external-store: specifier: latest - version: 1.5.0(react@19.1.1) + version: 1.6.0(react@19.2.3) devDependencies: '@babel/core': specifier: latest - version: 7.28.3 + version: 7.28.5 '@babel/plugin-transform-react-jsx': specifier: latest - version: 7.27.1(@babel/core@7.28.3) + version: 7.27.1(@babel/core@7.28.5) '@eslint/js': specifier: ^9.16.0 version: 9.30.1 '@testing-library/jest-dom': specifier: latest - version: 6.8.0 + version: 6.9.1 '@testing-library/react': specifier: latest - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.7(@types/react@19.1.11))(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@testing-library/user-event': specifier: latest version: 14.6.1(@testing-library/dom@10.4.1) @@ -113,19 +113,19 @@ importers: version: 24.0.13 '@types/react': specifier: latest - version: 19.1.11 + version: 19.2.7 '@types/react-dom': specifier: latest - version: 19.1.7(@types/react@19.1.11) + version: 19.2.3(@types/react@19.2.7) '@types/use-sync-external-store': specifier: latest version: 1.5.0 '@vitejs/plugin-react-oxc': specifier: latest - version: 0.4.1(rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0)) + version: 0.4.3(rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0)) '@vitejs/plugin-react-swc': specifier: latest - version: 4.0.1(rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0)) + version: 4.2.2(rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0)) eslint: specifier: ^9.9.0 version: 9.30.1 @@ -149,10 +149,10 @@ importers: version: 5.8.3 vite: specifier: npm:rolldown-vite@latest - version: rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0) + version: rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0) vitest: specifier: latest - version: 3.2.4(@types/node@24.0.13)(@vitest/ui@3.2.4)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) + version: 4.0.16(@types/node@24.0.13)(@vitest/ui@4.0.16)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) packages/react: dependencies: @@ -161,17 +161,17 @@ importers: version: link:../lib react: specifier: latest - version: 19.1.1 + version: 19.2.3 react-dom: specifier: latest - version: 19.1.1(react@19.1.1) + version: 19.2.3(react@19.2.3) devDependencies: '@babel/core': specifier: latest - version: 7.28.3 + version: 7.28.5 '@babel/plugin-transform-react-jsx': specifier: latest - version: 7.27.1(@babel/core@7.28.3) + version: 7.27.1(@babel/core@7.28.5) '@eslint/js': specifier: ^9.16.0 version: 9.30.1 @@ -180,16 +180,16 @@ importers: version: 24.0.13 '@types/react': specifier: latest - version: 19.1.11 + version: 19.2.7 '@types/react-dom': specifier: latest - version: 19.1.7(@types/react@19.1.11) + version: 19.2.3(@types/react@19.2.7) '@types/use-sync-external-store': specifier: latest version: 1.5.0 '@vitejs/plugin-react': specifier: latest - version: 5.0.1(rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0)) + version: 5.1.2(rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0)) compression: specifier: ^1.7.5 version: 1.8.1 @@ -228,10 +228,13 @@ importers: version: 5.8.3 vite: specifier: npm:rolldown-vite@latest - version: rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0) + version: rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0) packages/vanilla: dependencies: + '@mswjs/http-middleware': + specifier: ^0.10.3 + version: 0.10.3(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3)) compression: specifier: ^1.8.1 version: 1.8.1 @@ -247,7 +250,7 @@ importers: version: 9.30.1 '@playwright/test': specifier: latest - version: 1.55.0 + version: 1.57.0 '@testing-library/dom': specifier: ^10.4.0 version: 10.4.1 @@ -259,10 +262,10 @@ importers: version: 14.6.1(@testing-library/dom@10.4.1) '@vitest/coverage-v8': specifier: latest - version: 3.2.4(vitest@3.2.4) + version: 4.0.16(vitest@4.0.16) '@vitest/ui': specifier: ^2.1.8 - version: 2.1.9(vitest@3.2.4) + version: 2.1.9(vitest@4.0.16) concurrently: specifier: latest version: 9.2.1 @@ -298,20 +301,16 @@ importers: version: 3.6.2 vite: specifier: npm:rolldown-vite@latest - version: rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0) + version: rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0) vitest: specifier: latest - version: 3.2.4(@types/node@24.0.13)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) + version: 4.0.16(@types/node@24.0.13)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) packages: '@adobe/css-tools@4.4.3': resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -323,14 +322,18 @@ packages: resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.3': - resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} '@babel/generator@7.28.3': resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -365,21 +368,25 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.3': - resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.0': - resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.3': - resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -419,8 +426,8 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.3': - resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} '@babel/types@7.28.0': @@ -431,6 +438,10 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -472,14 +483,14 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@emnapi/core@1.4.5': - resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/runtime@1.4.5': - resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - '@emnapi/wasi-threads@1.0.4': - resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} @@ -724,17 +735,12 @@ packages: '@types/node': optional: true - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -742,15 +748,27 @@ packages: '@jridgewell/sourcemap-codec@1.5.4': resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mswjs/http-middleware@0.10.3': + resolution: {integrity: sha512-6CoX9IivDF7hggORdA4vX6uz+pkY1urGQMhmviHmYya/0b4EXwmhaXlGLQG3G29heqb3qdjp61V0+E2xRtyR5A==} + engines: {node: '>=18'} + peerDependencies: + msw: '>=2.0.0' + '@mswjs/interceptors@0.39.2': resolution: {integrity: sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@1.0.3': - resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} + '@napi-rs/wasm-runtime@1.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -773,104 +791,107 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxc-project/runtime@0.82.3': - resolution: {integrity: sha512-LNh5GlJvYHAnMurO+EyA8jJwN1rki7l3PSHuosDh2I7h00T6/u9rCkUjg/SvPmT1CZzvhuW0y+gf7jcqUy/Usg==} - engines: {node: '>=6.9.0'} - - '@oxc-project/types@0.82.3': - resolution: {integrity: sha512-6nCUxBnGX0c6qfZW5MaF6/fmu5dHJDMiMPaioKHKs5mi5+8/FHQ7WGjgQIz1zxpmceMYfdIXkOaLYE+ejbuOtA==} + '@oxc-project/runtime@0.101.0': + resolution: {integrity: sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==} + engines: {node: ^20.19.0 || >=22.12.0} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} + '@oxc-project/types@0.101.0': + resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} '@pkgr/core@0.2.7': resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.55.0': - resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==} + '@playwright/test@1.57.0': + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} engines: {node: '>=18'} hasBin: true '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rolldown/binding-android-arm64@1.0.0-beta.34': - resolution: {integrity: sha512-jf5GNe5jP3Sr1Tih0WKvg2bzvh5T/1TA0fn1u32xSH7ca/p5t+/QRr4VRFCV/na5vjwKEhwWrChsL2AWlY+eoA==} + '@rolldown/binding-android-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.34': - resolution: {integrity: sha512-2F/TqH4QuJQ34tgWxqBjFL3XV1gMzeQgUO8YRtCPGBSP0GhxtoFzsp7KqmQEothsxztlv+KhhT9Dbg3HHwHViQ==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.34': - resolution: {integrity: sha512-E1QuFslgLWbHQ8Qli/AqUKdfg0pockQPwRxVbhNQ74SciZEZpzLaujkdmOLSccMlSXDfFCF8RPnMoRAzQ9JV8Q==} + '@rolldown/binding-darwin-x64@1.0.0-beta.53': + resolution: {integrity: sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.34': - resolution: {integrity: sha512-VS8VInNCwnkpI9WeQaWu3kVBq9ty6g7KrHdLxYMzeqz24+w9hg712TcWdqzdY6sn+24lUoMD9jTZrZ/qfVpk0g==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.53': + resolution: {integrity: sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.34': - resolution: {integrity: sha512-4St4emjcnULnxJYb/5ZDrH/kK/j6PcUgc3eAqH5STmTrcF+I9m/X2xvSF2a2bWv1DOQhxBewThu0KkwGHdgu5w==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': + resolution: {integrity: sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.34': - resolution: {integrity: sha512-a737FTqhFUoWfnebS2SnQ2BS50p0JdukdkUBwy2J06j4hZ6Eej0zEB8vTfAqoCjn8BQKkXBy+3Sx0IRkgwz1gA==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': + resolution: {integrity: sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.34': - resolution: {integrity: sha512-NH+FeQWKyuw0k+PbXqpFWNfvD8RPvfJk766B/njdaWz4TmiEcSB0Nb6guNw1rBpM1FmltQYb3fFnTumtC6pRfA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': + resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.34': - resolution: {integrity: sha512-Q3RSCivp8pNadYK8ke3hLnQk08BkpZX9BmMjgwae2FWzdxhxxUiUzd9By7kneUL0vRQ4uRnhD9VkFQ+Haeqdvw==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': + resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.34': - resolution: {integrity: sha512-wDd/HrNcVoBhWWBUW3evJHoo7GJE/RofssBy3Dsiip05YUBmokQVrYAyrboOY4dzs/lJ7HYeBtWQ9hj8wlyF0A==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': + resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.34': - resolution: {integrity: sha512-dH3FTEV6KTNWpYSgjSXZzeX7vLty9oBYn6R3laEdhwZftQwq030LKL+5wyQdlbX5pnbh4h127hpv3Hl1+sj8dg==} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.34': - resolution: {integrity: sha512-y5BUf+QtO0JsIDKA51FcGwvhJmv89BYjUl8AmN7jqD6k/eU55mH6RJYnxwCsODq5m7KSSTigVb6O7/GqB8wbPw==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': + resolution: {integrity: sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': - resolution: {integrity: sha512-ga5hFhdTwpaNxEiuxZHWnD3ed0GBAzbgzS5tRHpe0ObptxM1a9Xrq6TVfNQirBLwb5Y7T/FJmJi3pmdLy95ljg==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': + resolution: {integrity: sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.34': - resolution: {integrity: sha512-4/MBp9T9eRnZskxWr8EXD/xHvLhdjWaeX/qY9LPRG1JdCGV3DphkLTy5AWwIQ5jhAy2ZNJR5z2fYRlpWU0sIyQ==} - cpu: [ia32] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.34': - resolution: {integrity: sha512-7O5iUBX6HSBKlQU4WykpUoEmb0wQmonb6ziKFr3dJTHud2kzDnWMqk344T0qm3uGv9Ddq6Re/94pInxo1G2d4w==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': + resolution: {integrity: sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.32': - resolution: {integrity: sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==} + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - '@rolldown/pluginutils@1.0.0-beta.34': - resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} '@rollup/rollup-android-arm-eabi@4.44.2': resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==} @@ -972,6 +993,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/core-darwin-arm64@1.13.5': resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} engines: {node: '>=10'} @@ -1055,8 +1079,12 @@ packages: resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.1': + resolution: {integrity: sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -1076,8 +1104,8 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tybys/wasm-util@0.10.0': - resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1112,13 +1140,13 @@ packages: '@types/node@24.0.13': resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==} - '@types/react-dom@19.1.7': - resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 - '@types/react@19.1.11': - resolution: {integrity: sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==} + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -1188,41 +1216,41 @@ packages: resolution: {integrity: sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitejs/plugin-react-oxc@0.4.1': - resolution: {integrity: sha512-aEJYLO6UwO3mCrK0MHXELZo+SqORbFp+HhP70fdVMtb101liQq1h7NT5jy6/NbwlvpQTJm1jRm7QkiuzGjoVjQ==} + '@vitejs/plugin-react-oxc@0.4.3': + resolution: {integrity: sha512-eJv6hHOIOVXzA4b2lZwccu/7VNmk9372fGOqsx5tNxiJHLtFBokyCTQUhlgjjXxl7guLPauHp0TqGTVyn1HvQA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^6.3.0 || ^7.0.0 - '@vitejs/plugin-react-swc@4.0.1': - resolution: {integrity: sha512-NQhPjysi5duItyrMd5JWZFf2vNOuSMyw+EoZyTBDzk+DkfYD8WNrsUs09sELV2cr1P15nufsN25hsUBt4CKF9Q==} + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitejs/plugin-react@5.0.1': - resolution: {integrity: sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==} + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - '@vitest/coverage-v8@3.2.4': - resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + '@vitest/coverage-v8@4.0.16': + resolution: {integrity: sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==} peerDependencies: - '@vitest/browser': 3.2.4 - vitest: 3.2.4 + '@vitest/browser': 4.0.16 + vitest: 4.0.16 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.0.16': + resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + '@vitest/mocker@4.0.16': + resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true @@ -1232,33 +1260,37 @@ packages: '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.0.16': + resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.0.16': + resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==} - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.0.16': + resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==} - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.0.16': + resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} '@vitest/ui@2.1.9': resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==} peerDependencies: vitest: 2.1.9 - '@vitest/ui@3.2.4': - resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} + '@vitest/ui@4.0.16': + resolution: {integrity: sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg==} peerDependencies: - vitest: 3.2.4 + vitest: 4.0.16 '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.0.16': + resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -1309,10 +1341,6 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - ansis@4.1.0: - resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} - engines: {node: '>=14'} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1323,16 +1351,15 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - ast-v8-to-istanbul@0.3.4: - resolution: {integrity: sha512-cxrAnZNLBnQwBPByK4CeDaw5sWZtMilJE/Q3iDA0aamgaIVNDF9T6K2/8DfYDZEejZ2jNnDrG9m8MY72HFd0KA==} + ast-v8-to-istanbul@0.3.9: + resolution: {integrity: sha512-dSC6tJeOJxbZrPzPbv5mMd6CMiQ1ugaVXXPRad2fXUSsy1kstFn9XQWemV9VW7Y7kpxgQ/4WMoZfwdH8XSU48w==} async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1343,6 +1370,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -1366,10 +1397,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1385,8 +1412,8 @@ packages: caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} - chai@5.2.1: - resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + chai@6.2.1: + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} chalk@4.1.2: @@ -1397,10 +1424,6 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -1454,6 +1477,10 @@ packages: engines: {node: '>=18'} hasBin: true + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -1465,6 +1492,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -1484,8 +1514,8 @@ packages: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} @@ -1511,10 +1541,6 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1530,6 +1556,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -1548,9 +1578,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -1566,9 +1593,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -1711,6 +1735,10 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -1770,6 +1798,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -1793,10 +1825,6 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} @@ -1805,6 +1833,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -1863,10 +1895,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1923,6 +1951,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1940,6 +1972,10 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2023,13 +2059,10 @@ packages: resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2087,68 +2120,74 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} lilconfig@3.1.3: @@ -2192,11 +2231,11 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + magicast@0.5.1: + resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} @@ -2210,10 +2249,17 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -2225,6 +2271,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2245,6 +2295,11 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -2264,10 +2319,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -2300,6 +2351,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@0.6.4: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} @@ -2322,6 +2377,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2368,9 +2426,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2394,9 +2449,8 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -2415,10 +2469,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2443,13 +2493,13 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - playwright-core@1.55.0: - resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} hasBin: true - playwright@1.55.0: - resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==} + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} engines: {node: '>=18'} hasBin: true @@ -2499,24 +2549,28 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + raw-body@3.0.0: resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} - react-dom@19.1.1: - resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: - react: ^19.1.1 + react: ^19.2.3 react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-refresh@0.17.0: - resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} - react@19.1.1: - resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} redent@3.0.0: @@ -2545,13 +2599,13 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rolldown-vite@7.1.5: - resolution: {integrity: sha512-NgHjKatQn1B5TjtNVS3+Uq3JBUPP8s70cMxLzGHpv/UyCGj0SQUtVYImNWbU2uqfOpNSnqhI+nbR7tmPPcb1qQ==} + rolldown-vite@7.3.0: + resolution: {integrity: sha512-5hI5NCJwKBGtzWtdKB3c2fOEpI77Iaa0z4mSzZPU1cJ/OqrGbFafm90edVCd7T9Snz+Sh09TMAv4EQqyVLzuEg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - esbuild: ^0.25.0 + esbuild: ^0.27.0 jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 @@ -2585,8 +2639,9 @@ packages: yaml: optional: true - rolldown@1.0.0-beta.34: - resolution: {integrity: sha512-Wwh7EwalMzzX3Yy3VN58VEajeR2Si8+HDNMf706jPLIqU7CxneRW+dQVfznf5O0TWTnJyu4npelwg2bzTXB1Nw==} + rolldown@1.0.0-beta.53: + resolution: {integrity: sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true rollup@4.44.2: @@ -2620,8 +2675,8 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -2632,10 +2687,18 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -2682,6 +2745,10 @@ packages: resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2709,8 +2776,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -2723,10 +2790,6 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -2751,9 +2814,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@3.0.0: - resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} - strip-outer@1.0.1: resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} engines: {node: '>=0.10.0'} @@ -2773,34 +2833,27 @@ packages: resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} engines: {node: ^14.18.0 || >=16.0.0} - test-exclude@7.0.1: - resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} - engines: {node: '>=18'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.3: - resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} tldts-core@6.1.86: @@ -2863,6 +2916,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -2906,20 +2963,19 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - vite@6.0.3: resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2960,26 +3016,32 @@ packages: yaml: optional: true - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vitest@4.0.16: + resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.16 + '@vitest/browser-preview': 4.0.16 + '@vitest/browser-webdriverio': 4.0.16 + '@vitest/ui': 4.0.16 happy-dom: '*' jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': optional: true '@vitest/ui': optional: true @@ -3030,10 +3092,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.0: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} @@ -3092,11 +3150,6 @@ snapshots: '@adobe/css-tools@4.4.3': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -3113,18 +3166,18 @@ snapshots: '@babel/compat-data@7.28.0': {} - '@babel/core@7.28.3': + '@babel/core@7.28.5': dependencies: - '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) - '@babel/helpers': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -3141,6 +3194,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.0 @@ -3162,12 +3223,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.3 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -3177,43 +3238,45 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.3': + '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - - '@babel/parser@7.28.0': - dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.5 '@babel/parser@7.28.3': dependencies: '@babel/types': 7.28.2 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)': + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.3 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.3)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.3 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.3)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.3 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.3)': + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.3 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) '@babel/types': 7.28.0 transitivePeerDependencies: - supports-color @@ -3223,8 +3286,8 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@babel/traverse@7.28.0': dependencies: @@ -3238,14 +3301,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.3': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -3260,6 +3323,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} '@bundled-es-modules/cookie@2.0.1': @@ -3295,18 +3363,18 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@emnapi/core@1.4.5': + '@emnapi/core@1.7.1': dependencies: - '@emnapi/wasi-threads': 1.0.4 + '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.5': + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.0.4': + '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 optional: true @@ -3473,31 +3541,40 @@ snapshots: optionalDependencies: '@types/node': 24.0.13 - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@istanbuljs/schema@0.1.3': {} - '@jridgewell/gen-mapping@0.3.12': dependencies: '@jridgewell/sourcemap-codec': 1.5.4 '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@mswjs/http-middleware@0.10.3(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))': + dependencies: + express: 4.22.1 + msw: 2.10.3(@types/node@24.0.13)(typescript@5.8.3) + strict-event-emitter: 0.5.1 + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.39.2': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -3507,11 +3584,11 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/wasm-runtime@1.0.3': + '@napi-rs/wasm-runtime@1.1.0': dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.4.5 - '@tybys/wasm-util': 0.10.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 optional: true '@nodelib/fs.scandir@2.1.5': @@ -3535,68 +3612,62 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxc-project/runtime@0.82.3': {} + '@oxc-project/runtime@0.101.0': {} - '@oxc-project/types@0.82.3': {} - - '@pkgjs/parseargs@0.11.0': - optional: true + '@oxc-project/types@0.101.0': {} '@pkgr/core@0.2.7': {} - '@playwright/test@1.55.0': + '@playwright/test@1.57.0': dependencies: - playwright: 1.55.0 + playwright: 1.57.0 '@polka/url@1.0.0-next.29': {} - '@rolldown/binding-android-arm64@1.0.0-beta.34': + '@rolldown/binding-android-arm64@1.0.0-beta.53': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.34': + '@rolldown/binding-darwin-arm64@1.0.0-beta.53': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.34': + '@rolldown/binding-darwin-x64@1.0.0-beta.53': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.34': + '@rolldown/binding-freebsd-x64@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.34': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.34': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.34': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.34': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.34': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.34': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.34': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': dependencies: - '@napi-rs/wasm-runtime': 1.0.3 - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': + '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.34': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.34': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': optional: true - '@rolldown/pluginutils@1.0.0-beta.32': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rolldown/pluginutils@1.0.0-beta.34': {} + '@rolldown/pluginutils@1.0.0-beta.53': {} '@rollup/rollup-android-arm-eabi@4.44.2': optional: true @@ -3658,6 +3729,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.44.2': optional: true + '@standard-schema/spec@1.1.0': {} + '@swc/core-darwin-arm64@1.13.5': optional: true @@ -3730,21 +3803,30 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.7(@types/react@19.1.11))(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.27.6 '@testing-library/dom': 10.4.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.11 - '@types/react-dom': 19.1.7(@types/react@19.1.11) + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 - '@tybys/wasm-util@0.10.0': + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true @@ -3788,13 +3870,13 @@ snapshots: dependencies: undici-types: 7.8.0 - '@types/react-dom@19.1.7(@types/react@19.1.11)': + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: - '@types/react': 19.1.11 + '@types/react': 19.2.7 - '@types/react@19.1.11': + '@types/react@19.2.7': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 '@types/statuses@2.0.6': {} @@ -3894,92 +3976,88 @@ snapshots: '@typescript-eslint/types': 8.36.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react-oxc@0.4.1(rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0))': + '@vitejs/plugin-react-oxc@0.4.3(rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.32 - vite: rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0) + '@rolldown/pluginutils': 1.0.0-beta.47 + vite: rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0) - '@vitejs/plugin-react-swc@4.0.1(rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@4.2.2(rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.32 + '@rolldown/pluginutils': 1.0.0-beta.47 '@swc/core': 1.13.5 - vite: rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0) + vite: rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.0.1(rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0))': + '@vitejs/plugin-react@5.1.2(rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0))': dependencies: - '@babel/core': 7.28.3 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3) - '@rolldown/pluginutils': 1.0.0-beta.32 + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0) + react-refresh: 0.18.0 + vite: rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': + '@vitest/coverage-v8@4.0.16(vitest@4.0.16)': dependencies: - '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.4 - debug: 4.4.1 + '@vitest/utils': 4.0.16 + ast-v8-to-istanbul: 0.3.9 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.9.0 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.0.13)(@vitest/ui@3.2.4)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) + istanbul-reports: 3.2.0 + magicast: 0.5.1 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.16(@types/node@24.0.13)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/expect@3.2.4': + '@vitest/expect@4.0.16': dependencies: + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.2 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - tinyrainbow: 2.0.0 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 + chai: 6.2.1 + tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0))': + '@vitest/mocker@4.0.16(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.2)(yaml@2.8.0))': dependencies: - '@vitest/spy': 3.2.4 + '@vitest/spy': 4.0.16 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.21 optionalDependencies: msw: 2.10.3(@types/node@24.0.13)(typescript@5.8.3) - vite: 6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0) + vite: 6.0.3(@types/node@24.0.13)(lightningcss@1.30.2)(yaml@2.8.0) '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 - '@vitest/pretty-format@3.2.4': + '@vitest/pretty-format@4.0.16': dependencies: - tinyrainbow: 2.0.0 + tinyrainbow: 3.0.3 - '@vitest/runner@3.2.4': + '@vitest/runner@4.0.16': dependencies: - '@vitest/utils': 3.2.4 + '@vitest/utils': 4.0.16 pathe: 2.0.3 - strip-literal: 3.0.0 - '@vitest/snapshot@3.2.4': + '@vitest/snapshot@4.0.16': dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.17 + '@vitest/pretty-format': 4.0.16 + magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.3 + '@vitest/spy@4.0.16': {} - '@vitest/ui@2.1.9(vitest@3.2.4)': + '@vitest/ui@2.1.9(vitest@4.0.16)': dependencies: '@vitest/utils': 2.1.9 fflate: 0.8.2 @@ -3988,18 +4066,18 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 1.2.0 - vitest: 3.2.4(@types/node@24.0.13)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) + vitest: 4.0.16(@types/node@24.0.13)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) - '@vitest/ui@3.2.4(vitest@3.2.4)': + '@vitest/ui@4.0.16(vitest@4.0.16)': dependencies: - '@vitest/utils': 3.2.4 + '@vitest/utils': 4.0.16 fflate: 0.8.2 flatted: 3.3.3 pathe: 2.0.3 - sirv: 3.0.1 - tinyglobby: 0.2.14 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.0.13)(@vitest/ui@3.2.4)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vitest: 4.0.16(@types/node@24.0.13)(@vitest/ui@4.0.16)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0) '@vitest/utils@2.1.9': dependencies: @@ -4007,11 +4085,15 @@ snapshots: loupe: 3.1.4 tinyrainbow: 1.2.0 - '@vitest/utils@3.2.4': + '@vitest/utils@4.0.16': dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.1.4 - tinyrainbow: 2.0.0 + '@vitest/pretty-format': 4.0.16 + tinyrainbow: 3.0.3 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 accepts@2.0.0: dependencies: @@ -4053,8 +4135,6 @@ snapshots: ansi-styles@6.2.1: {} - ansis@4.1.0: {} - argparse@2.0.1: {} aria-query@5.3.0: @@ -4063,13 +4143,13 @@ snapshots: aria-query@5.3.2: {} - array-union@2.1.0: {} + array-flatten@1.1.1: {} - assertion-error@2.0.1: {} + array-union@2.1.0: {} - ast-v8-to-istanbul@0.3.4: + ast-v8-to-istanbul@0.3.9: dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 js-tokens: 9.0.1 @@ -4079,6 +4159,23 @@ snapshots: balanced-match@1.0.2: {} + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.2.0: dependencies: bytes: 3.1.2 @@ -4115,8 +4212,6 @@ snapshots: bytes@3.1.2: {} - cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4131,13 +4226,7 @@ snapshots: caniuse-lite@1.0.30001727: {} - chai@5.2.1: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.4 - pathval: 2.0.1 + chai@6.2.1: {} chalk@4.1.2: dependencies: @@ -4146,8 +4235,6 @@ snapshots: chalk@5.4.1: {} - check-error@2.1.1: {} - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -4208,6 +4295,10 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -4216,6 +4307,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -4233,7 +4326,7 @@ snapshots: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} + csstype@3.2.3: {} data-urls@5.0.0: dependencies: @@ -4250,8 +4343,6 @@ snapshots: decimal.js@10.6.0: {} - deep-eql@5.0.2: {} - deep-is@0.1.4: {} delayed-stream@1.0.0: {} @@ -4260,6 +4351,8 @@ snapshots: dequal@2.0.3: {} + destroy@1.2.0: {} + detect-libc@2.0.4: {} dir-glob@3.0.1: @@ -4276,8 +4369,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} electron-to-chromium@1.5.180: {} @@ -4288,8 +4379,6 @@ snapshots: emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} entities@6.0.1: {} @@ -4459,6 +4548,42 @@ snapshots: expect-type@1.2.2: {} + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + express@5.1.0: dependencies: accepts: 2.0.0 @@ -4537,6 +4662,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.0: dependencies: debug: 4.4.1 @@ -4571,11 +4708,6 @@ snapshots: flatted@3.3.3: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -4586,6 +4718,8 @@ snapshots: forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} fs-extra@11.3.0: @@ -4646,15 +4780,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - globals@14.0.0: {} globals@15.15.0: {} @@ -4704,6 +4829,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -4722,6 +4855,10 @@ snapshots: husky@9.1.7: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -4785,17 +4922,11 @@ snapshots: transitivePeerDependencies: - supports-color - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -4885,50 +5016,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lightningcss-darwin-arm64@1.30.1: + lightningcss-android-arm64@1.30.2: optional: true - lightningcss-darwin-x64@1.30.1: + lightningcss-darwin-arm64@1.30.2: optional: true - lightningcss-freebsd-x64@1.30.1: + lightningcss-darwin-x64@1.30.2: optional: true - lightningcss-linux-arm-gnueabihf@1.30.1: + lightningcss-freebsd-x64@1.30.2: optional: true - lightningcss-linux-arm64-gnu@1.30.1: + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true - lightningcss-linux-arm64-musl@1.30.1: + lightningcss-linux-arm64-gnu@1.30.2: optional: true - lightningcss-linux-x64-gnu@1.30.1: + lightningcss-linux-arm64-musl@1.30.2: optional: true - lightningcss-linux-x64-musl@1.30.1: + lightningcss-linux-x64-gnu@1.30.2: optional: true - lightningcss-win32-arm64-msvc@1.30.1: + lightningcss-linux-x64-musl@1.30.2: optional: true - lightningcss-win32-x64-msvc@1.30.1: + lightningcss-win32-arm64-msvc@1.30.2: optional: true - lightningcss@1.30.1: + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: dependencies: detect-libc: 2.0.4 optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 lilconfig@3.1.3: {} @@ -4984,14 +5119,14 @@ snapshots: lz-string@1.5.0: {} - magic-string@0.30.17: + magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.3.5: + magicast@0.5.1: dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 source-map-js: 1.2.1 make-dir@3.1.0: @@ -5004,14 +5139,20 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@0.3.0: {} + media-typer@1.1.0: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5029,6 +5170,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} @@ -5043,8 +5186,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minipass@7.1.2: {} - mrmime@2.0.1: {} ms@2.0.0: {} @@ -5082,6 +5223,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + negotiator@0.6.4: {} negotiator@1.0.0: {} @@ -5096,6 +5239,8 @@ snapshots: object-inspect@1.13.4: {} + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5143,8 +5288,6 @@ snapshots: p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5161,10 +5304,7 @@ snapshots: path-key@4.0.0: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 + path-to-regexp@0.1.12: {} path-to-regexp@6.3.0: {} @@ -5176,8 +5316,6 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.1: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5192,11 +5330,11 @@ snapshots: dependencies: find-up: 4.1.0 - playwright-core@1.55.0: {} + playwright-core@1.57.0: {} - playwright@1.55.0: + playwright@1.57.0: dependencies: - playwright-core: 1.55.0 + playwright-core: 1.57.0 optionalDependencies: fsevents: 2.3.2 @@ -5241,6 +5379,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.0: dependencies: bytes: 3.1.2 @@ -5248,16 +5393,16 @@ snapshots: iconv-lite: 0.6.3 unpipe: 1.0.0 - react-dom@19.1.1(react@19.1.1): + react-dom@19.2.3(react@19.2.3): dependencies: - react: 19.1.1 - scheduler: 0.26.0 + react: 19.2.3 + scheduler: 0.27.0 react-is@17.0.2: {} - react-refresh@0.17.0: {} + react-refresh@0.18.0: {} - react@19.1.1: {} + react@19.2.3: {} redent@3.0.0: dependencies: @@ -5279,40 +5424,38 @@ snapshots: rfdc@1.4.1: {} - rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0): + rolldown-vite@7.3.0(@types/node@24.0.13)(yaml@2.8.0): dependencies: + '@oxc-project/runtime': 0.101.0 fdir: 6.5.0(picomatch@4.0.3) - lightningcss: 1.30.1 + lightningcss: 1.30.2 picomatch: 4.0.3 postcss: 8.5.6 - rolldown: 1.0.0-beta.34 - tinyglobby: 0.2.14 + rolldown: 1.0.0-beta.53 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.0.13 fsevents: 2.3.3 yaml: 2.8.0 - rolldown@1.0.0-beta.34: + rolldown@1.0.0-beta.53: dependencies: - '@oxc-project/runtime': 0.82.3 - '@oxc-project/types': 0.82.3 - '@rolldown/pluginutils': 1.0.0-beta.34 - ansis: 4.1.0 + '@oxc-project/types': 0.101.0 + '@rolldown/pluginutils': 1.0.0-beta.53 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.34 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.34 - '@rolldown/binding-darwin-x64': 1.0.0-beta.34 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.34 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.34 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.34 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.34 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.34 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.34 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.34 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.34 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.34 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.34 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.34 + '@rolldown/binding-android-arm64': 1.0.0-beta.53 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.53 + '@rolldown/binding-darwin-x64': 1.0.0-beta.53 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.53 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.53 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.53 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.53 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.53 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.53 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.53 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.53 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.53 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.53 rollup@4.44.2: dependencies: @@ -5370,12 +5513,30 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.26.0: {} + scheduler@0.27.0: {} semver@6.3.1: {} semver@7.7.2: {} + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + send@1.2.0: dependencies: debug: 4.4.1 @@ -5392,6 +5553,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -5449,6 +5619,12 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slash@3.0.0: {} slice-ansi@5.0.0: @@ -5469,7 +5645,7 @@ snapshots: statuses@2.0.2: {} - std-env@3.9.0: {} + std-env@3.10.0: {} strict-event-emitter@0.5.1: {} @@ -5481,12 +5657,6 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - string-width@7.2.0: dependencies: emoji-regex: 10.4.0 @@ -5509,10 +5679,6 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@3.0.0: - dependencies: - js-tokens: 9.0.1 - strip-outer@1.0.1: dependencies: escape-string-regexp: 1.0.5 @@ -5531,28 +5697,23 @@ snapshots: dependencies: '@pkgr/core': 0.2.7 - test-exclude@7.0.1: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 10.4.5 - minimatch: 9.0.5 - tinybench@2.9.0: {} - tinyexec@0.3.2: {} + tinyexec@1.0.2: {} tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@1.1.1: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 tinyrainbow@1.2.0: {} - tinyrainbow@2.0.0: {} - - tinyspy@4.0.3: {} + tinyrainbow@3.0.3: {} tldts-core@6.1.86: {} @@ -5603,6 +5764,11 @@ snapshots: type-fest@4.41.0: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -5644,34 +5810,15 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-sync-external-store@1.5.0(react@19.1.1): + use-sync-external-store@1.6.0(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 - vary@1.1.2: {} + utils-merge@1.0.1: {} - vite-node@3.2.4(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0): - dependencies: - cac: 6.7.14 - debug: 4.4.1 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml + vary@1.1.2: {} - vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0): + vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.2)(yaml@2.8.0): dependencies: esbuild: 0.24.2 postcss: 8.5.6 @@ -5679,37 +5826,34 @@ snapshots: optionalDependencies: '@types/node': 24.0.13 fsevents: 2.3.3 - lightningcss: 1.30.1 + lightningcss: 1.30.2 yaml: 2.8.0 - vitest@3.2.4(@types/node@24.0.13)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0): + vitest@4.0.16(@types/node@24.0.13)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0): dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - debug: 4.4.1 + '@vitest/expect': 4.0.16 + '@vitest/mocker': 4.0.16(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.2)(yaml@2.8.0)) + '@vitest/pretty-format': 4.0.16 + '@vitest/runner': 4.0.16 + '@vitest/snapshot': 4.0.16 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 + es-module-lexer: 1.7.0 expect-type: 1.2.2 - magic-string: 0.30.17 + magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.2 - std-env: 3.9.0 + picomatch: 4.0.3 + std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0) + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 6.0.3(@types/node@24.0.13)(lightningcss@1.30.2)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.0.13 - '@vitest/ui': 2.1.9(vitest@3.2.4) + '@vitest/ui': 2.1.9(vitest@4.0.16) jsdom: 25.0.1 transitivePeerDependencies: - jiti @@ -5720,39 +5864,35 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml - vitest@3.2.4(@types/node@24.0.13)(@vitest/ui@3.2.4)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0): + vitest@4.0.16(@types/node@24.0.13)(@vitest/ui@4.0.16)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(yaml@2.8.0): dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - debug: 4.4.1 + '@vitest/expect': 4.0.16 + '@vitest/mocker': 4.0.16(msw@2.10.3(@types/node@24.0.13)(typescript@5.8.3))(vite@6.0.3(@types/node@24.0.13)(lightningcss@1.30.2)(yaml@2.8.0)) + '@vitest/pretty-format': 4.0.16 + '@vitest/runner': 4.0.16 + '@vitest/snapshot': 4.0.16 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 + es-module-lexer: 1.7.0 expect-type: 1.2.2 - magic-string: 0.30.17 + magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.2 - std-env: 3.9.0 + picomatch: 4.0.3 + std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 6.0.3(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.0.13)(lightningcss@1.30.1)(yaml@2.8.0) + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 6.0.3(@types/node@24.0.13)(lightningcss@1.30.2)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.0.13 - '@vitest/ui': 3.2.4(vitest@3.2.4) + '@vitest/ui': 4.0.16(vitest@4.0.16) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -5763,7 +5903,6 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml @@ -5808,12 +5947,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - wrap-ansi@9.0.0: dependencies: ansi-styles: 6.2.1