diff --git a/package.json b/package.json index 1858a28b..62ae3f81 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/node": "^24.0.13", "@vitest/coverage-v8": "latest", "@vitest/ui": "latest", + "concurrently": "latest", "eslint": "^9.16.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", @@ -45,11 +46,11 @@ "globals": "^15.13.0", "husky": "^9.1.7", "lint-staged": "^15.2.11", + "nodemon": "^3.1.11", "prettier": "^3.4.2", "typescript": "^5.8.3", "typescript-eslint": "^8.36.0", "vite": "npm:rolldown-vite@latest", - "vitest": "latest", - "concurrently": "latest" + "vitest": "latest" } } diff --git a/packages/lib/src/Router.ts b/packages/lib/src/Router.ts index eb3bd157..40ba4e0f 100644 --- a/packages/lib/src/Router.ts +++ b/packages/lib/src/Router.ts @@ -12,6 +12,8 @@ type QueryPayload = Record; export type RouterInstance = InstanceType>; +const isServer = typeof window === "undefined"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export class Router any> { readonly #routes: Map>; @@ -25,29 +27,33 @@ export class Router any> { this.#route = null; this.#baseUrl = baseUrl.replace(/\/$/, ""); - window.addEventListener("popstate", () => { - this.#route = this.#findRoute(); - this.#observer.notify(); - }); - - document.addEventListener("click", (e) => { - const target = e.target as HTMLElement; - if (!target?.closest("[data-link]")) { - return; - } - e.preventDefault(); - const url = target.getAttribute("href") ?? target.closest("[data-link]")?.getAttribute("href"); - if (url) { - this.push(url); - } - }); + if (!isServer) { + window.addEventListener("popstate", () => { + this.#route = this.#findRoute(); + this.#observer.notify(); + }); + + document.addEventListener("click", (e) => { + const target = e.target as HTMLElement; + if (!target?.closest("[data-link]")) { + return; + } + e.preventDefault(); + const url = target.getAttribute("href") ?? target.closest("[data-link]")?.getAttribute("href"); + if (url) { + this.push(url); + } + }); + } } get query(): StringRecord { + if (isServer) return {}; return Router.parseQuery(window.location.search); } set query(newQuery: QueryPayload) { + if (isServer) return; const newUrl = Router.getUrl(newQuery, this.#baseUrl); this.push(newUrl); } @@ -85,8 +91,23 @@ export class Router any> { }); } - #findRoute(url = window.location.pathname) { - const { pathname } = new URL(url, window.location.origin); + #findRoute(url?: string) { + if (isServer && !url) return null; + + let pathname: string; + if (url) { + // URL에서 pathname만 추출 (쿼리 문자열 제거) + if (url.includes("://")) { + pathname = new URL(url).pathname; + } else if (url.includes("?")) { + pathname = url.split("?")[0]; + } else { + pathname = url; + } + } else { + pathname = window.location.pathname; + } + for (const [routePath, route] of this.#routes) { const match = pathname.match(route.regex); if (match) { @@ -107,6 +128,7 @@ export class Router any> { } push(url: string) { + if (isServer) return; try { // baseUrl이 없으면 자동으로 붙여줌 const fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url); @@ -126,12 +148,14 @@ export class Router any> { } start() { + if (isServer) return; this.#route = this.#findRoute(); this.#observer.notify(); } - static parseQuery = (search = window.location.search) => { - const params = new URLSearchParams(search); + static parseQuery = (search?: string) => { + if (isServer && !search) return {}; + const params = new URLSearchParams(search ?? window.location.search); const query: StringRecord = {}; for (const [key, value] of params) { query[key] = value; @@ -150,6 +174,7 @@ export class Router any> { }; static getUrl = (newQuery: QueryPayload, baseUrl = "") => { + if (isServer) return ""; const currentQuery = Router.parseQuery(); const updatedQuery = { ...currentQuery, ...newQuery }; diff --git a/packages/lib/src/createStorage.ts b/packages/lib/src/createStorage.ts index fdf2986c..5536cd56 100644 --- a/packages/lib/src/createStorage.ts +++ b/packages/lib/src/createStorage.ts @@ -1,7 +1,19 @@ import { createObserver } from "./createObserver.ts"; -export const createStorage = (key: string, storage = window.localStorage) => { - let data: T | null = JSON.parse(storage.getItem(key) ?? "null"); +const isServer = typeof window === "undefined"; + +const noopStorage: Storage = { + length: 0, + clear: () => {}, + getItem: () => null, + key: () => null, + removeItem: () => {}, + setItem: () => {}, +}; + +export const createStorage = (key: string, storage?: Storage) => { + const actualStorage = storage ?? (isServer ? noopStorage : window.localStorage); + let data: T | null = JSON.parse(actualStorage.getItem(key) ?? "null"); const { subscribe, notify } = createObserver(); const get = () => data; @@ -9,7 +21,7 @@ export const createStorage = (key: string, storage = window.localStorage) => const set = (value: T) => { try { data = value; - storage.setItem(key, JSON.stringify(data)); + actualStorage.setItem(key, JSON.stringify(data)); notify(); } catch (error) { console.error(`Error setting storage item for key "${key}":`, error); @@ -19,7 +31,7 @@ export const createStorage = (key: string, storage = window.localStorage) => const reset = () => { try { data = null; - storage.removeItem(key); + actualStorage.removeItem(key); notify(); } catch (error) { console.error(`Error removing storage item for key "${key}":`, error); diff --git a/packages/lib/src/hooks/useRouter.ts b/packages/lib/src/hooks/useRouter.ts index 4a40cb5d..120a43ac 100644 --- a/packages/lib/src/hooks/useRouter.ts +++ b/packages/lib/src/hooks/useRouter.ts @@ -7,5 +7,6 @@ const defaultSelector = (state: T) => state as unknown as S; export const useRouter = , S>(router: T, selector = defaultSelector) => { const shallowSelector = useShallowSelector(selector); - return useSyncExternalStore(router.subscribe, () => shallowSelector(router)); + const getSnapshot = () => shallowSelector(router); + return useSyncExternalStore(router.subscribe, getSnapshot, getSnapshot); }; diff --git a/packages/lib/src/hooks/useStorage.ts b/packages/lib/src/hooks/useStorage.ts index f620638c..1c51cd7a 100644 --- a/packages/lib/src/hooks/useStorage.ts +++ b/packages/lib/src/hooks/useStorage.ts @@ -4,5 +4,5 @@ import type { createStorage } from "../createStorage"; type Storage = ReturnType>; export const useStorage = (storage: Storage) => { - return useSyncExternalStore(storage.subscribe, storage.get); + return useSyncExternalStore(storage.subscribe, storage.get, storage.get); }; diff --git a/packages/lib/src/hooks/useStore.ts b/packages/lib/src/hooks/useStore.ts index 56fa8800..992f09cf 100644 --- a/packages/lib/src/hooks/useStore.ts +++ b/packages/lib/src/hooks/useStore.ts @@ -8,5 +8,6 @@ const defaultSelector = (state: T) => state as unknown as S; export const useStore = (store: Store, selector: (state: T) => S = defaultSelector) => { const shallowSelector = useShallowSelector(selector); - return useSyncExternalStore(store.subscribe, () => shallowSelector(store.getState())); + const getSnapshot = () => shallowSelector(store.getState()); + return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot); }; diff --git a/packages/react/server.js b/packages/react/server.js index 81e3e1d8..1a3be71e 100644 --- a/packages/react/server.js +++ b/packages/react/server.js @@ -1,32 +1,102 @@ import express from "express"; -import { renderToString } from "react-dom/server"; -import { createElement } from "react"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const prod = process.env.NODE_ENV === "production"; const port = process.env.PORT || 5174; -const base = process.env.BASE || (prod ? "/front_7th_chapter4-1/react/" : "/"); +const base = prod ? "/front_7th_chapter4-1/react/" : "/"; const app = express(); -app.get("*all", (req, res) => { - res.send( - ` - - - - - - React SSR - - -
${renderToString(createElement("div", null, "안녕하세요"))}
- - - `.trim(), - ); -}); - -// Start http server -app.listen(port, () => { - console.log(`React Server started at http://localhost:${port}`); -}); +let template; +let render; + +if (prod) { + template = fs.readFileSync(path.resolve(__dirname, "dist/react/index.html"), "utf-8"); + const ssrModule = await import("./dist/react-ssr/main-server.js"); + render = ssrModule.render; +} else { + const { createServer: createViteServer } = await import("vite"); + const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: "custom", + }); + + // HTML이 아닌 파일들에 대해서만 Vite 미들웨어 사용 + app.use((req, res, next) => { + const url = req.originalUrl; + // HTML 요청이 아닌 경우만 Vite 미들웨어로 전달 + if (url.includes(".") && !url.endsWith(".html")) { + return vite.middlewares(req, res, next); + } + next(); + }); + + // 모든 HTML 요청에 대해 SSR 처리 + app.use(async (req, res, next) => { + const url = req.originalUrl.replace(base, "/"); + + try { + template = fs.readFileSync(path.resolve(__dirname, "index.html"), "utf-8"); + template = await vite.transformIndexHtml(url, template); + + const { render } = await vite.ssrLoadModule("/src/main-server.tsx"); + + const query = req.query; + const { html, head, initialData } = await render(url, query); + + const initialDataScript = initialData + ? `` + : ""; + + const finalHtml = template + .replace("", html) + .replace("", head) + .replace("", `${initialDataScript}`); + + res.status(200).set({ "Content-Type": "text/html" }).send(finalHtml); + } catch (e) { + vite.ssrFixStacktrace(e); + console.error(e); + next(e); + } + }); + + app.listen(port, () => { + console.log(`React Dev SSR Server started at http://localhost:${port}`); + }); +} + +if (prod) { + app.use(base, express.static(path.resolve(__dirname, "dist/react"), { index: false })); + + app.use("*all", async (req, res) => { + const url = req.originalUrl; + + try { + const query = req.query; + const { html, head, initialData } = await render(url, query); + + const initialDataScript = initialData + ? `` + : ""; + + const finalHtml = template + .replace("", html) + .replace("", head) + .replace("", `${initialDataScript}`); + + res.status(200).set({ "Content-Type": "text/html" }).send(finalHtml); + } catch (e) { + console.error(e); + res.status(500).send("Internal Server Error"); + } + }); + + app.listen(port, () => { + console.log(`React SSR Server started at http://localhost:${port}`); + }); +} diff --git a/packages/react/src/App.tsx b/packages/react/src/App.tsx index 36b302ca..29ded389 100644 --- a/packages/react/src/App.tsx +++ b/packages/react/src/App.tsx @@ -2,6 +2,7 @@ import { router, useCurrentPage } from "./router"; import { HomePage, NotFoundPage, ProductDetailPage } from "./pages"; import { useLoadCartStore } from "./entities"; import { ModalProvider, ToastProvider } from "./components"; +import { QueryProvider } from "./contexts/QueryContext"; // 홈 페이지 (상품 목록) router.addRoute("/", HomePage); @@ -16,15 +17,16 @@ const CartInitializer = () => { /** * 전체 애플리케이션 렌더링 */ -export const App = () => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const App = ({ initialData }: { initialData?: any }) => { const PageComponent = useCurrentPage(); return ( - <> + - {PageComponent ? : null} + {PageComponent ? : null} - + ); }; diff --git a/packages/react/src/contexts/QueryContext.tsx b/packages/react/src/contexts/QueryContext.tsx new file mode 100644 index 00000000..cc977a04 --- /dev/null +++ b/packages/react/src/contexts/QueryContext.tsx @@ -0,0 +1,62 @@ +import { createContext, useContext, type ReactNode } from "react"; +import type { StringRecord } from "@hanghae-plus/lib"; + +interface QueryContextType { + query: StringRecord; + updateQuery: (newQuery: StringRecord) => void; +} + +export const QueryContext = createContext(null); + +interface QueryProviderProps { + children: ReactNode; + initialQuery?: StringRecord; +} + +export function QueryProvider({ children, initialQuery = {} }: QueryProviderProps) { + const updateQuery = (newQuery: StringRecord) => { + // 클라이언트에서만 URL 업데이트 + if (typeof window !== "undefined") { + const url = new URL(window.location.href); + Object.entries(newQuery).forEach(([key, value]) => { + if (value === null || value === undefined || value === "") { + url.searchParams.delete(key); + } else { + url.searchParams.set(key, String(value)); + } + }); + window.history.pushState(null, "", url.toString()); + // 라우터에게 변경 알림 + window.dispatchEvent(new PopStateEvent("popstate")); + } + }; + + return {children}; +} + +export function useQueryContext(): QueryContextType { + const context = useContext(QueryContext); + if (!context) { + // Context가 없으면 기본값 반환 (CSR에서 Provider 없이 사용 가능하도록) + return { + query: {} as StringRecord, + updateQuery: (newQuery: StringRecord) => { + if (typeof window !== "undefined") { + const currentParams = new URLSearchParams(window.location.search); + Object.entries(newQuery).forEach(([key, value]) => { + if (value === null || value === undefined || value === "") { + currentParams.delete(key); + } else { + currentParams.set(key, String(value)); + } + }); + const queryString = currentParams.toString(); + const newUrl = `${window.location.pathname}${queryString ? `?${queryString}` : ""}`; + window.history.pushState(null, "", newUrl); + window.dispatchEvent(new PopStateEvent("popstate")); + } + }, + }; + } + return context; +} diff --git a/packages/react/src/entities/products/components/SearchBar.tsx b/packages/react/src/entities/products/components/SearchBar.tsx index 19f9dc9f..2615d873 100644 --- a/packages/react/src/entities/products/components/SearchBar.tsx +++ b/packages/react/src/entities/products/components/SearchBar.tsx @@ -12,44 +12,34 @@ const OPTION_SORTS = [ { value: "name_desc", label: "이름 역순" }, ]; -// 검색 입력 (Enter 키) -const handleSearchKeyDown = async (e: KeyboardEvent) => { - if (e.key === "Enter") { - const query = e.currentTarget.value.trim(); - try { +export function SearchBar() { + const { categories } = useProductStore(); + const { searchQuery, limit = "20", sort, category } = useProductFilter(); + + // 검색 입력 (Enter 키) + const handleSearchKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + const query = e.currentTarget.value.trim(); searchProducts(query); - } catch (error) { - console.error("검색 실패:", error); } - } -}; - -// 페이지당 상품 수 변경 -const handleLimitChange = async (e: ChangeEvent) => { - const limit = parseInt(e.target.value); - try { - setLimit(limit); - } catch (error) { - console.error("상품 수 변경 실패:", error); - } -}; + }; -// 정렬 변경 -const handleSortChange = async (e: ChangeEvent) => { - const sort = e.target.value; + // 페이지당 상품 수 변경 + const handleLimitChange = (e: ChangeEvent) => { + const newLimit = parseInt(e.target.value); + setLimit(newLimit); + }; - try { - setSort(sort); - } catch (error) { - console.error("정렬 변경 실패:", error); - } -}; + // 정렬 변경 + const handleSortChange = (e: ChangeEvent) => { + const newSort = e.target.value; + setSort(newSort); + }; -// 브레드크럼 카테고리 네비게이션 -const handleBreadCrumbClick = async (e: MouseEvent) => { - const breadcrumbType = e.currentTarget.getAttribute("data-breadcrumb"); + // 브레드크럼 카테고리 네비게이션 + const handleBreadCrumbClick = (e: MouseEvent) => { + const breadcrumbType = e.currentTarget.getAttribute("data-breadcrumb"); - try { if (breadcrumbType === "reset") { // "전체" 클릭 -> 카테고리 초기화 setCategory({ category1: "", category2: "" }); @@ -58,38 +48,22 @@ const handleBreadCrumbClick = async (e: MouseEvent) => { const category1 = e.currentTarget.getAttribute("data-category1"); setCategory({ ...(category1 && { category1 }), category2: "" }); } - } catch (error) { - console.error("브레드크럼 네비게이션 실패:", error); - } -}; - -// 1depth 카테고리 선택 -const handleMainCategoryClick = async (e: MouseEvent) => { - const category1 = e.currentTarget.getAttribute("data-category1"); - if (!category1) return; + }; - try { + // 1depth 카테고리 선택 + const handleMainCategoryClick = (e: MouseEvent) => { + const category1 = e.currentTarget.getAttribute("data-category1"); + if (!category1) return; setCategory({ category1, category2: "" }); - } catch (error) { - console.error("1depth 카테고리 선택 실패:", error); - } -}; + }; -const handleSubCategoryClick = async (e: MouseEvent) => { - const category1 = e.currentTarget.getAttribute("data-category1"); - const category2 = e.currentTarget.getAttribute("data-category2"); - if (!category1 || !category2) return; - - try { + // 2depth 카테고리 선택 + const handleSubCategoryClick = (e: MouseEvent) => { + const category1 = e.currentTarget.getAttribute("data-category1"); + const category2 = e.currentTarget.getAttribute("data-category2"); + if (!category1 || !category2) return; setCategory({ category1, category2 }); - } catch (error) { - console.error("2depth 카테고리 선택 실패:", error); - } -}; - -export function SearchBar() { - const { categories } = useProductStore(); - const { searchQuery, limit = "20", sort, category } = useProductFilter(); + }; const categoryList = Object.keys(categories).length > 0 ? Object.keys(categories) : []; const limitOptions = OPTION_LIMITS.map((value) => ( @@ -121,6 +95,7 @@ export function SearchBar() {
{ const productId = useRouterParams((params) => params.id); + const initialLoadDone = useRef(false); + useEffect(() => { - loadProductDetailForPage(productId); + const state = productStore.getState(); + // 서버에서 이미 해당 상품 데이터를 로드했다면 API 호출 스킵 + const hasServerData = state.currentProduct?.productId === productId && state.status === "done"; + + if (!hasServerData || initialLoadDone.current) { + loadProductDetailForPage(productId); + } + initialLoadDone.current = true; }, [productId]); }; diff --git a/packages/react/src/entities/products/components/hooks/useProductFilter.ts b/packages/react/src/entities/products/components/hooks/useProductFilter.ts index ab24c1ec..c33624b8 100644 --- a/packages/react/src/entities/products/components/hooks/useProductFilter.ts +++ b/packages/react/src/entities/products/components/hooks/useProductFilter.ts @@ -1,14 +1,98 @@ -import { useEffect } from "react"; -import { useRouterQuery } from "../../../../router"; +import { useEffect, useRef, useState } from "react"; import { loadProducts } from "../../productUseCase"; +import { productStore } from "../../productStore"; +import { router } from "../../../../router"; +import { useQueryContext } from "../../../../contexts/QueryContext"; + +// 서버 환경 확인 +const isServer = typeof window === "undefined"; + +// URL에서 쿼리 파라미터 파싱 +const parseQuery = () => { + if (isServer) return {}; + const params = new URLSearchParams(window.location.search); + const query: Record = {}; + params.forEach((value, key) => { + query[key] = value; + }); + return query; +}; export const useProductFilter = () => { - const { search: searchQuery, limit, sort, category1, category2 } = useRouterQuery(); + // SSR용 QueryContext에서 쿼리 가져오기 + const { query: ssrQuery } = useQueryContext(); + + // CSR에서 URL 변경 감지를 위한 state + const [urlQuery, setUrlQuery] = useState>(parseQuery); + + // URL 변경 감지 효과 + useEffect(() => { + if (isServer) return; + + const handleUrlChange = () => { + setUrlQuery(parseQuery()); + }; + + // router 구독 + const unsubscribe = router.subscribe(handleUrlChange); + + // popstate 이벤트도 구독 + window.addEventListener("popstate", handleUrlChange); + + return () => { + unsubscribe(); + window.removeEventListener("popstate", handleUrlChange); + }; + }, []); + + // 서버에서는 ssrQuery 사용, 클라이언트에서는 urlQuery 사용 + const query = isServer ? ssrQuery : urlQuery; + + const searchQuery = query.search || query.query || ""; + const limit = query.limit || "20"; + const sort = query.sort || "price_asc"; + const category1 = query.category1 || ""; + const category2 = query.category2 || ""; + const current = query.current || "1"; + const category = { category1, category2 }; + const didMount = useRef(false); + const prevCurrentRef = useRef(current); + useEffect(() => { - loadProducts(true); - }, [searchQuery, limit, sort, category1, category2]); + // 서버에서는 실행하지 않음 + if (isServer) return; + + const state = productStore.getState(); + + // 첫 마운트 시 SSR 데이터가 있으면 스킵 + if (!didMount.current) { + didMount.current = true; + if (state.products.length > 0 && state.status === "done") { + prevCurrentRef.current = current; + return; + } + } + + // current만 변경된 경우 (무한 스크롤) 스킵 - loadMoreProducts에서 직접 처리함 + if (prevCurrentRef.current !== current) { + prevCurrentRef.current = current; + // 다른 필터는 변경되지 않았으면 스킵 + return; + } + + // 현재 쿼리를 직접 전달 + const currentQuery = { + search: searchQuery, + limit, + sort, + category1, + category2, + current: "1", // 필터 변경 시 항상 첫 페이지로 + }; + loadProducts(true, currentQuery); + }, [searchQuery, limit, sort, category1, category2, current]); return { searchQuery, diff --git a/packages/react/src/entities/products/productUseCase.ts b/packages/react/src/entities/products/productUseCase.ts index fa967b56..b780f8a4 100644 --- a/packages/react/src/entities/products/productUseCase.ts +++ b/packages/react/src/entities/products/productUseCase.ts @@ -3,6 +3,9 @@ import { router } from "../../router"; import type { StringRecord } from "../../types"; import { initialProductState, PRODUCT_ACTIONS, productStore } from "./productStore"; import { isNearBottom } from "../../utils"; +import { Router } from "@hanghae-plus/lib"; +import type { FunctionComponent } from "react"; +import { useQueryContext } from "../../contexts/QueryContext"; const createErrorMessage = (error: unknown, defaultMessage = "알 수 없는 오류 발생") => error instanceof Error ? error.message : defaultMessage; @@ -47,17 +50,20 @@ export const loadProductsAndCategories = async () => { } }; -export const loadProducts = async (resetList = true) => { +export const loadProducts = async (resetList = true, queryOverride?: StringRecord) => { try { productStore.dispatch({ type: PRODUCT_ACTIONS.SETUP, payload: { loading: true, status: "pending", error: null }, }); + // queryOverride가 있으면 사용, 없으면 router.query 사용 + const query = queryOverride || router.query; + const { products, pagination: { total }, - } = await getProducts(router.query); + } = await getProducts(query); const payload = { products, totalCount: total }; // 페이지 리셋이면 새로 설정, 아니면 기존에 추가 @@ -83,23 +89,34 @@ export const loadMoreProducts = async () => { return; } - router.query = { current: Number(router.query.current ?? 1) + 1 }; - await loadProducts(false); + // 현재 쿼리 유지하면서 페이지만 증가 + const currentQuery = router.query; + const newQuery = { ...currentQuery, current: String(Number(currentQuery.current ?? 1) + 1) }; + const newUrl = Router.getUrl(newQuery); + (router as Router).push(newUrl); + + // 새 쿼리를 직접 전달 + await loadProducts(false, newQuery); }; + export const searchProducts = (search: string) => { - router.query = { search, current: 1 }; + const newUrl = Router.getUrl({ search, current: 1 }); + (router as Router).push(newUrl); }; export const setCategory = (categoryData: StringRecord) => { - router.query = { ...categoryData, current: 1 }; + const newUrl = Router.getUrl({ ...categoryData, current: 1 }); + (router as Router).push(newUrl); }; export const setSort = (sort: string) => { - router.query = { sort, current: 1 }; + const newUrl = Router.getUrl({ sort, current: 1 }); + (router as Router).push(newUrl); }; export const setLimit = (limit: number) => { - router.query = { limit, current: 1 }; + const newUrl = Router.getUrl({ limit, current: 1 }); + (router as Router).push(newUrl); }; export const loadProductDetailForPage = async (productId: string) => { @@ -194,3 +211,53 @@ export const loadNextProducts = async () => { } } }; + +// Context 기반 검색 함수들 (Hook 형태) +export const useSearchProducts = () => { + const { updateQuery } = useQueryContext(); + + return (search: string) => { + updateQuery({ search, current: 1 }); + }; +}; + +export const useSetCategory = () => { + const { updateQuery } = useQueryContext(); + + return (categoryData: StringRecord) => { + updateQuery({ ...categoryData, current: 1 }); + }; +}; + +export const useSetSort = () => { + const { updateQuery } = useQueryContext(); + + return (sort: string) => { + updateQuery({ sort, current: 1 }); + }; +}; + +export const useSetLimit = () => { + const { updateQuery } = useQueryContext(); + + return (limit: number) => { + updateQuery({ limit, current: 1 }); + }; +}; + +export const useLoadMoreProducts = () => { + const { query, updateQuery } = useQueryContext(); + + return async () => { + const state = productStore.getState(); + const hasMore = state.products.length < state.totalCount; + + if (!hasMore || state.loading) { + return; + } + + const newQuery = { ...query, current: Number(query.current ?? 1) + 1 }; + updateQuery(newQuery); + await loadProducts(false); + }; +}; diff --git a/packages/react/src/hydration.ts b/packages/react/src/hydration.ts new file mode 100644 index 00000000..91126e5a --- /dev/null +++ b/packages/react/src/hydration.ts @@ -0,0 +1,63 @@ +import { productStore, PRODUCT_ACTIONS } from "./entities/products/productStore"; +import type { HomePageData, ProductDetailData, InitialData } from "./ssr-data"; + +declare global { + interface Window { + __INITIAL_DATA__?: InitialData; + } +} + +function isHomePageData(data: InitialData): data is HomePageData { + return data !== null && "products" in data && "categories" in data && "totalCount" in data; +} + +function isProductDetailData(data: InitialData): data is ProductDetailData { + return data !== null && "currentProduct" in data && "relatedProducts" in data; +} + +export function hydrateFromServerData(): boolean { + if (typeof window === "undefined") { + return false; + } + + const initialData = window.__INITIAL_DATA__; + + if (!initialData) { + return false; + } + + if (isHomePageData(initialData)) { + productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + products: initialData.products, + categories: initialData.categories, + totalCount: initialData.totalCount, + loading: false, + status: "done", + }, + }); + return true; + } + + if (isProductDetailData(initialData)) { + productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + currentProduct: initialData.currentProduct, + relatedProducts: initialData.relatedProducts, + loading: false, + status: "done", + }, + }); + return true; + } + + return false; +} + +export function cleanupInitialData(): void { + if (typeof window !== "undefined") { + delete window.__INITIAL_DATA__; + } +} diff --git a/packages/react/src/main-server.tsx b/packages/react/src/main-server.tsx index 611b0a58..6621a6aa 100644 --- a/packages/react/src/main-server.tsx +++ b/packages/react/src/main-server.tsx @@ -1,4 +1,105 @@ -export const render = async (url: string, query: Record) => { - console.log({ url, query }); - return ""; +import { renderToString } from "react-dom/server"; +import { ServerRouter } from "./router/serverRouter"; +import { HomePage, NotFoundPage, ProductDetailPage } from "./pages"; +import { productStore, PRODUCT_ACTIONS, initialProductState } from "./entities/products/productStore"; +import { ModalProvider, ToastProvider } from "./components"; +import { loadHomePageData, loadProductDetailData, type InitialData } from "./ssr-data"; +import type { StringRecord } from "./types"; +import { QueryProvider } from "./contexts/QueryContext"; + +const BASE_URL = process.env.NODE_ENV === "production" ? "/front_7th_chapter4-1/react" : ""; + +const serverRouter = new ServerRouter(BASE_URL); + +serverRouter.addRoute("/", HomePage); +serverRouter.addRoute("/product/:id/", ProductDetailPage); +serverRouter.addRoute(".*", NotFoundPage); + +const ServerApp = ({ router, query }: { router: ServerRouter; query: StringRecord }) => { + const PageComponent = router.target; + + return ( + + + {PageComponent ? : null} + + + ); +}; + +function generateHead(url: string): string { + const isProductDetail = url.includes("/product/"); + + if (isProductDetail) { + const product = productStore.getState().currentProduct; + if (product) { + return ` + ${product.title} - 쇼핑몰 + + `; + } + } + + return ` + 쇼핑몰 - 홈 + + `; +} + +export const render = async ( + url: string, + query: StringRecord, +): Promise<{ html: string; head: string; initialData: InitialData }> => { + serverRouter.navigate(url, query); + + let initialData: InitialData = null; + + productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + ...initialProductState, + loading: false, + status: "done", + }, + }); + + const isHomePage = serverRouter.route?.path === "/"; + const isProductDetail = serverRouter.route?.path === "/product/:id/"; + + if (isHomePage) { + const homeData = await loadHomePageData(query); + initialData = homeData; + + productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + products: homeData.products, + categories: homeData.categories, + totalCount: homeData.totalCount, + loading: false, + status: "done", + }, + }); + } else if (isProductDetail) { + const productId = serverRouter.params.id; + if (productId) { + const productData = await loadProductDetailData(productId); + initialData = productData; + + productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + currentProduct: productData.currentProduct, + relatedProducts: productData.relatedProducts, + loading: false, + status: "done", + }, + }); + } + } + + const html = renderToString(); + const head = generateHead(url); + + return { html, head, initialData }; }; diff --git a/packages/react/src/main.tsx b/packages/react/src/main.tsx index 0c5b8a67..5272c7dc 100644 --- a/packages/react/src/main.tsx +++ b/packages/react/src/main.tsx @@ -1,7 +1,8 @@ import { App } from "./App"; import { router } from "./router"; import { BASE_URL } from "./constants.ts"; -import { createRoot } from "react-dom/client"; +import { createRoot, hydrateRoot } from "react-dom/client"; +import { hydrateFromServerData, cleanupInitialData } from "./hydration"; const enableMocking = () => import("./mocks/browser").then(({ worker }) => @@ -17,7 +18,15 @@ function main() { router.start(); const rootElement = document.getElementById("root")!; - createRoot(rootElement).render(); + const hasServerRenderedContent = rootElement.innerHTML.trim() !== "" && rootElement.innerHTML !== ""; + + if (hasServerRenderedContent) { + hydrateFromServerData(); + hydrateRoot(rootElement, ); + cleanupInitialData(); + } else { + createRoot(rootElement).render(); + } } // 애플리케이션 시작 diff --git a/packages/react/src/pages/HomePage.tsx b/packages/react/src/pages/HomePage.tsx index 4edbccc6..c14147fe 100644 --- a/packages/react/src/pages/HomePage.tsx +++ b/packages/react/src/pages/HomePage.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { loadNextProducts, loadProductsAndCategories, ProductList, SearchBar } from "../entities"; +import { loadNextProducts, loadProductsAndCategories, ProductList, SearchBar, productStore } from "../entities"; import { PageWrapper } from "./PageWrapper"; const headerLeft = ( @@ -29,7 +29,12 @@ const unregisterScrollHandler = () => { export const HomePage = () => { useEffect(() => { registerScrollHandler(); - loadProductsAndCategories(); + const state = productStore.getState(); + + // 스토어가 비어있을 때만 초기 로드 (SSR 데이터가 없는 경우) + if (!state.products || state.products.length === 0) { + loadProductsAndCategories(); + } return unregisterScrollHandler; }, []); diff --git a/packages/react/src/router/RouterContext.tsx b/packages/react/src/router/RouterContext.tsx new file mode 100644 index 00000000..11bd7f7d --- /dev/null +++ b/packages/react/src/router/RouterContext.tsx @@ -0,0 +1,23 @@ +import { createContext, useContext, type ReactNode } from "react"; +import type { StringRecord } from "../types"; + +interface RouterContextValue { + query: StringRecord; + params: StringRecord; +} + +const RouterContext = createContext(null); + +interface RouterProviderProps { + children: ReactNode; + query?: StringRecord; + params?: StringRecord; +} + +export function RouterProvider({ children, query = {}, params = {} }: RouterProviderProps) { + return {children}; +} + +export function useServerRouterContext(): RouterContextValue | null { + return useContext(RouterContext); +} diff --git a/packages/react/src/router/hooks/useRouterQuery.ts b/packages/react/src/router/hooks/useRouterQuery.ts index 261299da..2ed1899a 100644 --- a/packages/react/src/router/hooks/useRouterQuery.ts +++ b/packages/react/src/router/hooks/useRouterQuery.ts @@ -1,6 +1,18 @@ -import { useRouter } from "@hanghae-plus/lib"; +import { useRouter, type RouterInstance } from "@hanghae-plus/lib"; import { router } from "../router"; +import { useContext } from "react"; +import { QueryContext } from "../../contexts/QueryContext"; +import type { StringRecord, AnyFunction } from "@hanghae-plus/lib"; -export const useRouterQuery = () => { - return useRouter(router, ({ query }) => query); +export const useRouterQuery = (): StringRecord => { + // Context를 안전하게 가져오기 (Provider 없어도 에러 안남) + const contextValue = useContext(QueryContext); + const routerQuery = useRouter(router as RouterInstance, ({ query }) => query); + + // SSR에서는 Context 사용, CSR에서는 라우터 사용 + if (typeof window === "undefined") { + return (contextValue?.query as StringRecord) || {}; + } + + return routerQuery; }; diff --git a/packages/react/src/router/index.ts b/packages/react/src/router/index.ts index 704bc03c..bbe0e7c7 100644 --- a/packages/react/src/router/index.ts +++ b/packages/react/src/router/index.ts @@ -1,2 +1,4 @@ export * from "./router"; +export * from "./serverRouter"; export * from "./hooks"; +export * from "./RouterContext"; diff --git a/packages/react/src/router/serverRouter.ts b/packages/react/src/router/serverRouter.ts new file mode 100644 index 00000000..fc227735 --- /dev/null +++ b/packages/react/src/router/serverRouter.ts @@ -0,0 +1,140 @@ +import type { FunctionComponent } from "react"; +import type { StringRecord } from "../types"; + +interface Route { + regex: RegExp; + paramNames: string[]; + handler: FunctionComponent; + params?: StringRecord; +} + +export class ServerRouter { + readonly #routes: Map; + readonly #baseUrl: string; + + #route: (Route & { params: StringRecord; path: string }) | null = null; + #query: StringRecord = {}; + + constructor(baseUrl = "") { + this.#routes = new Map(); + this.#baseUrl = baseUrl.replace(/\/$/, ""); + } + + get query(): StringRecord { + return this.#query; + } + + set query(newQuery: StringRecord) { + this.#query = { ...this.#query, ...newQuery }; + } + + get params() { + return this.#route?.params ?? {}; + } + + get route() { + return this.#route; + } + + get target() { + return this.#route?.handler; + } + + subscribe = () => { + return () => {}; + }; + + addRoute(path: string, handler: FunctionComponent) { + const paramNames: string[] = []; + + // catch-all 라우트는 특별 처리 + if (path === ".*") { + this.#routes.set(path, { + regex: new RegExp(`^${this.#baseUrl}.*$`), + paramNames: [], + handler, + }); + return; + } + + let regexPath = path + .replace(/:\w+/g, (match) => { + paramNames.push(match.slice(1)); + return "([^/]+)"; + }) + .replace(/\//g, "\\/"); + + // "/" 라우트는 baseUrl만 매칭하도록 (선택적 trailing slash 포함) + if (path === "/") { + regexPath = "\\/?"; + } + + // trailing slash를 선택적으로 만듦 + if (regexPath.endsWith("\\/")) { + regexPath = regexPath.slice(0, -2) + "\\/?"; + } + + const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`); + + this.#routes.set(path, { + regex, + paramNames, + handler, + }); + } + + navigate(url: string, query: StringRecord = {}) { + this.#query = query; + let pathname = url.split("?")[0]; + + // 마지막 / 제거 (baseUrl만 있거나 루트 경로가 아닌 경우) + if (pathname.endsWith("/") && pathname !== "/") { + pathname = pathname.slice(0, -1); + } + + // .* (catch-all) 라우트는 마지막에 확인 + let catchAllRoute: [string, Route] | null = null; + + for (const [routePath, route] of this.#routes) { + // catch-all 라우트는 건너뛰고 나중에 확인 + if (routePath === ".*") { + catchAllRoute = [routePath, route]; + continue; + } + + const match = pathname.match(route.regex); + if (match) { + const params: StringRecord = {}; + route.paramNames.forEach((name, index) => { + params[name] = match[index + 1]; + }); + + this.#route = { + ...route, + params, + path: routePath, + }; + return; + } + } + + // 다른 라우트가 매칭되지 않으면 catch-all 확인 + if (catchAllRoute) { + const [routePath, route] = catchAllRoute; + const match = pathname.match(route.regex); + if (match) { + this.#route = { + ...route, + params: {}, + path: routePath, + }; + return; + } + } + + this.#route = null; + } + + push() {} + start() {} +} diff --git a/packages/react/src/ssr-data.ts b/packages/react/src/ssr-data.ts new file mode 100644 index 00000000..b4db5ac7 --- /dev/null +++ b/packages/react/src/ssr-data.ts @@ -0,0 +1,109 @@ +import type { Categories, Product } from "./entities/products/types"; +import type { StringRecord } from "./types"; +import items from "./mocks/items.json"; + +export interface HomePageData { + products: Product[]; + categories: Categories; + totalCount: number; + query?: StringRecord; +} + +export interface ProductDetailData { + currentProduct: Product | null; + relatedProducts: Product[]; +} + +export type InitialData = HomePageData | ProductDetailData | null; + +function getUniqueCategories(): Categories { + const categories: 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; +} + +function filterProducts(products: Product[], query: StringRecord): Product[] { + let filtered = [...products]; + + // query 파라미터는 'query' 또는 'search' 둘 다 지원 + const searchQuery = query.query || query.search; + if (searchQuery) { + const searchTerm = searchQuery.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); + } + + const sort = query.sort || "price_asc"; + switch (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; + } + + return filtered; +} + +export async function loadHomePageData(query: StringRecord = {}): Promise { + const page = parseInt(query.current ?? query.page ?? "1"); + const limit = parseInt(query.limit ?? "20"); + + const filteredProducts = filterProducts(items as Product[], query); + + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedProducts = filteredProducts.slice(startIndex, endIndex); + + const categories = getUniqueCategories(); + + return { + products: paginatedProducts, + categories, + totalCount: filteredProducts.length, + query, + }; +} + +export async function loadProductDetailData(productId: string): Promise { + const product = (items as Product[]).find((item) => item.productId === productId); + + if (!product) { + return { + currentProduct: null, + relatedProducts: [], + }; + } + + const relatedProducts = (items as Product[]) + .filter((item) => item.category2 === product.category2 && item.productId !== productId) + .slice(0, 20); + + return { + currentProduct: product, + relatedProducts, + }; +} diff --git a/packages/react/src/utils/domUtils.ts b/packages/react/src/utils/domUtils.ts index 5cbe2255..92bb57f5 100644 --- a/packages/react/src/utils/domUtils.ts +++ b/packages/react/src/utils/domUtils.ts @@ -1,4 +1,5 @@ export const isNearBottom = (threshold = 200) => { + if (typeof window === "undefined") return false; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; diff --git a/packages/react/src/utils/log.ts b/packages/react/src/utils/log.ts index 00aa2d47..7ca5ead0 100644 --- a/packages/react/src/utils/log.ts +++ b/packages/react/src/utils/log.ts @@ -6,12 +6,18 @@ declare global { } } -window.__spyCalls = []; -window.__spyCallsClear = () => { +const isServer = typeof window === "undefined"; + +if (!isServer) { window.__spyCalls = []; -}; + window.__spyCallsClear = () => { + window.__spyCalls = []; + }; +} export const log: typeof console.log = (...args) => { - window.__spyCalls.push(args); + if (!isServer) { + window.__spyCalls.push(args); + } return console.log(...args); }; diff --git a/packages/react/static-site-generate.js b/packages/react/static-site-generate.js index 145c957b..dc91ac9a 100644 --- a/packages/react/static-site-generate.js +++ b/packages/react/static-site-generate.js @@ -1,17 +1,74 @@ -import { renderToString } from "react-dom/server"; -import { createElement } from "react"; import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +// SSG는 항상 production 모드로 실행 +process.env.NODE_ENV = "production"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DIST_DIR = path.resolve(__dirname, "../../dist/react"); async function generateStaticSite() { + // SSR 모듈 불러오기 + const ssrModule = await import("./dist/react-ssr/main-server.js"); + const render = ssrModule.render; + // HTML 템플릿 읽기 - const template = fs.readFileSync("../../dist/react/index.html", "utf-8"); + const template = fs.readFileSync(path.join(DIST_DIR, "index.html"), "utf-8"); + + // 생성할 페이지 목록 + const routes = [{ url: "/front_7th_chapter4-1/react/", query: {} }]; + + // 상품 목록에서 상품 ID 추출하여 상세 페이지 경로 추가 + const items = JSON.parse(fs.readFileSync(path.resolve(__dirname, "src/mocks/items.json"), "utf-8")); + + items.forEach((item) => { + routes.push({ + url: `/front_7th_chapter4-1/react/product/${item.productId}/`, + query: {}, + }); + }); + + console.log(`Generating ${routes.length} static pages...`); + + // 각 페이지 렌더링 및 저장 + for (const route of routes) { + try { + const { html, head, initialData } = await render(route.url, route.query); + + const initialDataScript = initialData + ? `` + : ""; + + const finalHtml = template + .replace("", html) + .replace("", head) + .replace("", `${initialDataScript}`); + + // 파일 경로 생성 + let filePath; + if (route.url === "/front_7th_chapter4-1/react/") { + filePath = path.join(DIST_DIR, "index.html"); + } else { + // /front_7th_chapter4-1/react/product/123/ -> product/123/index.html + const relativePath = route.url.replace("/front_7th_chapter4-1/react/", ""); + const dir = path.join(DIST_DIR, relativePath); + fs.mkdirSync(dir, { recursive: true }); + filePath = path.join(dir, "index.html"); + } + + fs.writeFileSync(filePath, finalHtml); + console.log(`Generated: ${filePath}`); + } catch (error) { + console.error(`Failed to generate ${route.url}:`, error); + } + } - // 어플리케이션 렌더링하기 - const appHtml = renderToString(createElement("div", null, "안녕하세요")); + // 404 페이지 복사 + const indexHtml = fs.readFileSync(path.join(DIST_DIR, "index.html"), "utf-8"); + fs.writeFileSync(path.join(DIST_DIR, "404.html"), indexHtml); - // 결과 HTML 생성하기 - const result = template.replace("", appHtml); - fs.writeFileSync("../../dist/react/index.html", result); + console.log("Static site generation complete!"); } // 실행 diff --git a/packages/vanilla/package.json b/packages/vanilla/package.json index ab5ae3fd..0de0982b 100644 --- a/packages/vanilla/package.json +++ b/packages/vanilla/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite --port 5173", "dev:ssr": "PORT=5174 node server.js", + "dev:ssr:watch": "PORT=5174 nodemon server.js", "build:client": "rm -rf ./dist/vanilla && vite build --outDir ./dist/vanilla && cp ./dist/vanilla/index.html ./dist/vanilla/404.html", "build:client-for-ssg": "rm -rf ../../dist/vanilla && vite build --outDir ../../dist/vanilla", "build:server": "vite build --outDir ./dist/vanilla-ssr --ssr src/main-server.js", diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js index 67f03afa..19fe2360 100644 --- a/packages/vanilla/server.js +++ b/packages/vanilla/server.js @@ -1,34 +1,140 @@ import express from "express"; +import { fileURLToPath } from "url"; +import { dirname, resolve } from "path"; +import fs from "fs/promises"; +import { server as mswServer } from "./src/mocks/node.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); const prod = process.env.NODE_ENV === "production"; const port = process.env.PORT || 5173; const base = process.env.BASE || (prod ? "/front_7th_chapter4-1/vanilla/" : "/"); -const app = express(); - -const render = () => { - return `
안녕하세요
`; -}; - -app.get("*all", (req, res) => { - res.send( - ` - - - - - - Vanilla Javascript SSR - - -
${render()}
- - - `.trim(), - ); +// MSW 서버 시작 (Node.js fetch 요청 인터셉트) +mswServer.listen({ + onUnhandledRequest: "bypass", }); -// Start http server -app.listen(port, () => { - console.log(`React Server started at http://localhost:${port}`); -}); +/** + * initialData 스크립트 태그 생성 + * @param {Object} initialData + * @returns {string} + */ +function createInitialDataScript(initialData) { + return ``; +} + +/** + * HTML 템플릿에 SSR 결과 삽입 + * @param {string} template + * @param {{ html: string, head: string, initialData: Object }} renderResult + * @returns {string} + */ +function applyTemplate(template, { html, head, initialData }) { + const initialDataScript = createInitialDataScript(initialData); + + return template + .replace("", head) + .replace("", html) + .replace("", `${initialDataScript}`); +} + +async function createServer() { + const app = express(); + + // HTML 템플릿과 render 함수 변수 + let template; + let render; + + if (!prod) { + // ============================================ + // 개발 환경: Vite dev server 사용 + // ============================================ + const { createServer: createViteServer } = await import("vite"); + + const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: "custom", + base, + }); + + // Vite 미들웨어 사용 + app.use(vite.middlewares); + + app.use(async (req, res, next) => { + const url = req.originalUrl.replace(base, "/"); + + try { + // 1. index.html 읽기 + template = await fs.readFile(resolve(__dirname, "index.html"), "utf-8"); + + // 2. Vite HTML 변환 적용 (HMR 클라이언트 주입 등) + template = await vite.transformIndexHtml(url, template); + + // 3. SSR 모듈 로드 (매 요청마다 최신 코드 반영) + const ssrModule = await vite.ssrLoadModule("/src/main-server.js"); + render = ssrModule.render; + + // 4. 렌더링 실행 + const renderResult = await render(url); + + // 5. 템플릿 적용 + const finalHtml = applyTemplate(template, renderResult); + + // 6. 응답 + res.status(200).set({ "Content-Type": "text/html" }).end(finalHtml); + } catch (e) { + // Vite 에러 처리 + vite.ssrFixStacktrace(e); + console.error(e); + next(e); + } + }); + } else { + // ============================================ + // 프로덕션 환경: 빌드된 파일 사용 + // ============================================ + const compression = (await import("compression")).default; + const sirv = (await import("sirv")).default; + + // gzip 압축 + app.use(compression()); + + // 정적 파일 서빙 (dist/vanilla) + app.use(base, sirv(resolve(__dirname, "dist/vanilla"), { extensions: [] })); + + // HTML 템플릿 미리 로드 + template = await fs.readFile(resolve(__dirname, "dist/vanilla/index.html"), "utf-8"); + + // SSR 모듈 로드 + const ssrModule = await import(resolve(__dirname, "dist/vanilla-ssr/main-server.js")); + render = ssrModule.render; + + app.use(async (req, res, next) => { + const url = req.originalUrl.replace(base, "/"); + + try { + // 1. 렌더링 실행 + const renderResult = await render(url); + + // 2. 템플릿 적용 + const finalHtml = applyTemplate(template, renderResult); + + // 3. 응답 + res.status(200).set({ "Content-Type": "text/html" }).end(finalHtml); + } catch (e) { + console.error(e); + next(e); + } + }); + } + + // 서버 시작 + app.listen(port, () => { + console.log(`Server started at http://localhost:${port}`); + console.log(`Environment: ${prod ? "production" : "development"}`); + }); +} + +createServer(); diff --git a/packages/vanilla/src/api/productApi.js b/packages/vanilla/src/api/productApi.js index c2330fbe..696501e4 100644 --- a/packages/vanilla/src/api/productApi.js +++ b/packages/vanilla/src/api/productApi.js @@ -1,3 +1,6 @@ +// 서버 환경에서는 절대 URL이 필요 (MSW가 인터셉트) +const BASE_URL = typeof window === "undefined" ? "http://localhost" : ""; + export async function getProducts(params = {}) { const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params; const page = params.current ?? params.page ?? 1; @@ -11,17 +14,17 @@ export async function getProducts(params = {}) { sort, }); - const response = await fetch(`/api/products?${searchParams}`); + const response = await fetch(`${BASE_URL}/api/products?${searchParams}`); return await response.json(); } export async function getProduct(productId) { - const response = await fetch(`/api/products/${productId}`); + const response = await fetch(`${BASE_URL}/api/products/${productId}`); return await response.json(); } export async function getCategories() { - const response = await fetch("/api/categories"); + const response = await fetch(`${BASE_URL}/api/categories`); return await response.json(); } diff --git a/packages/vanilla/src/lib/ServerRouter.js b/packages/vanilla/src/lib/ServerRouter.js new file mode 100644 index 00000000..44fda06b --- /dev/null +++ b/packages/vanilla/src/lib/ServerRouter.js @@ -0,0 +1,59 @@ +/** + * 서버 사이드 렌더링용 라우터 + * URL 패턴 매칭 및 파라미터 추출을 담당 + */ +export class ServerRouter { + constructor() { + this.routes = []; + } + + /** + * 라우트 추가 + * @param {string} path - 경로 패턴 (예: "/product/:id/") + * @param {string} handler - 페이지 식별자 + */ + addRoute(path, handler) { + // :param 형태를 정규식으로 변환 + const paramNames = []; + const regexPath = path.replace(/:([^/]+)/g, (_, paramName) => { + paramNames.push(paramName); + return "([^/]+)"; + }); + + this.routes.push({ + path, + regex: new RegExp(`^${regexPath}$`), + paramNames, + handler, + }); + } + + /** + * URL과 매칭되는 라우트 찾기 + * @param {string} url - URL 경로 + * @returns {{ handler: string, params: Object, path: string } | null} + */ + findRoute(url) { + // URL에서 경로만 추출 (쿼리스트링 제거) + const pathname = url.split("?")[0]; + + for (const route of this.routes) { + const match = pathname.match(route.regex); + if (match) { + // params 객체 생성 + const params = {}; + route.paramNames.forEach((name, index) => { + params[name] = match[index + 1]; + }); + + return { + handler: route.handler, + params, + path: route.path, + }; + } + } + + return null; + } +} diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js index 40b58858..22b19568 100644 --- a/packages/vanilla/src/main-server.js +++ b/packages/vanilla/src/main-server.js @@ -1,4 +1,136 @@ -export const render = async (url, query) => { - console.log({ url, query }); - return ""; -}; +import { PRODUCT_ACTIONS } from "./stores/actionTypes.js"; +import { getProducts, getProduct, getCategories } from "./api/productApi.js"; +import { ServerRouter } from "./lib/ServerRouter.js"; +import { createServerStores } from "./stores/createServerStore.js"; +import { parseUrl } from "./utils/ssrUtils.js"; +import { renderHomePage, renderProductDetailPage, renderNotFoundPage } from "./pages/server/index.js"; + +/** + * 서버 사이드 렌더링 메인 함수 + * @param {string} url - 요청 URL + * @returns {Promise<{ html: string, head: string, initialData: Object }>} + */ +export async function render(url) { + // URL 파싱 + const { pathname, query } = parseUrl(url); + + // 서버용 Store 초기화 + const stores = createServerStores(); + + // 라우터 설정 + const router = new ServerRouter(); + router.addRoute("/", "home"); + router.addRoute("/product/:id/", "product"); + router.addRoute("/404", "notfound"); + + // 라우트 매칭 + const route = router.findRoute(pathname); + const routePath = route?.path || "notfound"; + const params = route?.params || {}; + + // 데이터 프리페칭 + let initialData = {}; + + if (routePath === "/") { + // 홈페이지: 상품 목록 + 카테고리 + const limit = parseInt(query.limit) || 20; + const page = parseInt(query.page) || 1; + + const [productsData, categories] = await Promise.all([ + getProducts({ + page, + limit, + search: query.search || "", + category1: query.category1 || "", + category2: query.category2 || "", + sort: query.sort || "price_asc", + }), + getCategories(), + ]); + + stores.productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + products: productsData.products, + totalCount: productsData.pagination.total, + categories, + loading: false, + error: null, + }, + }); + + initialData = { + products: productsData.products, + categories, + totalCount: productsData.pagination.total, + }; + } else if (routePath === "/product/:id/") { + // 상품 상세 페이지 + const product = await getProduct(params.id); + + if (product && !product.error) { + // 관련 상품 조회 (같은 카테고리) + const relatedData = await getProducts({ + category1: product.category1, + limit: 20, + }); + const relatedProducts = relatedData.products.filter((p) => p.productId !== params.id); + + stores.productStore.dispatch({ + type: PRODUCT_ACTIONS.SET_CURRENT_PRODUCT, + payload: product, + }); + stores.productStore.dispatch({ + type: PRODUCT_ACTIONS.SET_RELATED_PRODUCTS, + payload: relatedProducts, + }); + + initialData = { + currentProduct: product, + relatedProducts, + }; + } else { + stores.productStore.dispatch({ + type: PRODUCT_ACTIONS.SET_ERROR, + payload: "상품을 찾을 수 없습니다.", + }); + } + } + + // HTML 렌더링 + let html = ""; + + if (routePath === "/") { + html = renderHomePage(stores, query); + } else if (routePath === "/product/:id/") { + html = renderProductDetailPage(stores); + } else { + html = renderNotFoundPage(stores); + } + + // SEO Head 태그 생성 + let head = ""; + + if (routePath === "/") { + head = `쇼핑몰 - 홈 + `; + } else if (routePath === "/product/:id/") { + const product = stores.productStore.getState().currentProduct; + if (product) { + head = ` + ${product.title} - 쇼핑몰 + + `; + } else { + head = ` + 상품을 찾을 수 없습니다 - 쇼핑몰 + `; + } + } else { + head = ` + 페이지를 찾을 수 없습니다 - 쇼핑몰 + `; + } + + return { html, head, initialData }; +} diff --git a/packages/vanilla/src/main.js b/packages/vanilla/src/main.js index 4c3f2765..49a44c4f 100644 --- a/packages/vanilla/src/main.js +++ b/packages/vanilla/src/main.js @@ -4,6 +4,7 @@ import { registerAllEvents } from "./events"; import { loadCartFromStorage } from "./services"; import { router } from "./router"; import { BASE_URL } from "./constants.js"; +import { productStore, PRODUCT_ACTIONS } from "./stores"; const enableMocking = () => import("./mocks/browser.js").then(({ worker }) => @@ -15,7 +16,53 @@ const enableMocking = () => }), ); +/** + * 서버에서 주입된 초기 데이터로 클라이언트 Store 복원 (Hydration) + */ +function hydrateInitialData() { + if (typeof window !== "undefined" && window.__INITIAL_DATA__) { + const data = window.__INITIAL_DATA__; + + console.log("__INITIAL_DATA__", data); + + // 홈페이지 데이터 복원 (상품 목록, 카테고리) + if (data.products) { + productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + products: data.products, + totalCount: data.totalCount, + categories: data.categories || {}, + loading: false, + error: null, + }, + }); + } + + // 상품 상세 페이지 데이터 복원 + if (data.currentProduct) { + productStore.dispatch({ + type: PRODUCT_ACTIONS.SET_CURRENT_PRODUCT, + payload: data.currentProduct, + }); + + if (data.relatedProducts) { + productStore.dispatch({ + type: PRODUCT_ACTIONS.SET_RELATED_PRODUCTS, + payload: data.relatedProducts, + }); + } + } + + // 사용 후 삭제 (메모리 정리 및 중복 hydration 방지) + delete window.__INITIAL_DATA__; + } +} + function main() { + // 서버 데이터로 Store 초기화 (Hydration) + hydrateInitialData(); + registerAllEvents(); registerGlobalEvents(); loadCartFromStorage(); diff --git a/packages/vanilla/src/mocks/handlers.js b/packages/vanilla/src/mocks/handlers.js index 6e3035e6..9cd19b09 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)); @@ -64,7 +64,7 @@ function filterProducts(products, query) { export const handlers = [ // 상품 목록 API - http.get("/api/products", async ({ request }) => { + http.get("*/api/products", async ({ request }) => { const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") ?? url.searchParams.get("current")) || 1; const limit = parseInt(url.searchParams.get("limit")) || 20; @@ -111,7 +111,7 @@ export const handlers = [ }), // 상품 상세 API - http.get("/api/products/:id", ({ params }) => { + http.get("*/api/products/:id", ({ params }) => { const { id } = params; const product = items.find((item) => item.productId === id); @@ -133,7 +133,7 @@ export const handlers = [ }), // 카테고리 목록 API - http.get("/api/categories", async () => { + http.get("*/api/categories", async () => { const categories = getUniqueCategories(); await delay(); return HttpResponse.json(categories); diff --git a/packages/vanilla/src/mocks/node.js b/packages/vanilla/src/mocks/node.js new file mode 100644 index 00000000..172ca7f8 --- /dev/null +++ b/packages/vanilla/src/mocks/node.js @@ -0,0 +1,4 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./handlers.js"; + +export const server = setupServer(...handlers); diff --git a/packages/vanilla/src/pages/server/ServerHomePage.js b/packages/vanilla/src/pages/server/ServerHomePage.js new file mode 100644 index 00000000..1f64b8a2 --- /dev/null +++ b/packages/vanilla/src/pages/server/ServerHomePage.js @@ -0,0 +1,40 @@ +import { ProductList, SearchBar } from "../../components/index.js"; +import { ServerPageWrapper } from "./ServerPageWrapper.js"; + +/** + * 서버용 HomePage 렌더링 + * @param {Object} stores - { productStore, cartStore, uiStore } + * @param {Object} query - URL 쿼리 파라미터 + * @returns {string} - 렌더링된 HTML + */ +export function renderHomePage(stores, query) { + const productState = stores.productStore.getState(); + const { search: searchQuery = "", limit = "20", sort = "", category1 = "", category2 = "" } = query; + const { products, loading, error, totalCount, categories } = productState; + const category = { category1, category2 }; + const hasMore = products.length < totalCount; + + return ServerPageWrapper( + { + headerLeft: ` +

+ 쇼핑몰 +

+ `.trim(), + children: ` + ${SearchBar({ searchQuery, limit, sort, category, categories })} + +
+ ${ProductList({ + products, + loading, + error, + totalCount, + hasMore, + })} +
+ `.trim(), + }, + stores, + ); +} diff --git a/packages/vanilla/src/pages/server/ServerNotFoundPage.js b/packages/vanilla/src/pages/server/ServerNotFoundPage.js new file mode 100644 index 00000000..6d17ca90 --- /dev/null +++ b/packages/vanilla/src/pages/server/ServerNotFoundPage.js @@ -0,0 +1,44 @@ +import { Logo } from "../../components/index.js"; +import { ServerPageWrapper } from "./ServerPageWrapper.js"; + +/** + * 서버용 NotFoundPage 렌더링 + * @param {Object} stores - { productStore, cartStore, uiStore } + * @returns {string} - 렌더링된 HTML + */ +export function renderNotFoundPage(stores) { + return ServerPageWrapper( + { + headerLeft: Logo(), + children: ` +
+ + + + + + + + + + + + 404 + + + + + + + 페이지를 찾을 수 없습니다 + + + + + 홈으로 +
+ `.trim(), + }, + stores, + ); +} diff --git a/packages/vanilla/src/pages/server/ServerPageWrapper.js b/packages/vanilla/src/pages/server/ServerPageWrapper.js new file mode 100644 index 00000000..661c601a --- /dev/null +++ b/packages/vanilla/src/pages/server/ServerPageWrapper.js @@ -0,0 +1,50 @@ +import { CartModal, Footer, Toast } from "../../components/index.js"; + +/** + * 서버용 PageWrapper + * @param {{ headerLeft: string, children: string }} props + * @param {Object} stores - { productStore, cartStore, uiStore } + * @returns {string} - 렌더링된 HTML + */ +export function ServerPageWrapper({ headerLeft, children }, stores) { + const cart = stores.cartStore.getState(); + const { cartModal, toast } = stores.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/pages/server/ServerProductDetailPage.js b/packages/vanilla/src/pages/server/ServerProductDetailPage.js new file mode 100644 index 00000000..2bc68874 --- /dev/null +++ b/packages/vanilla/src/pages/server/ServerProductDetailPage.js @@ -0,0 +1,249 @@ +import { ServerPageWrapper } from "./ServerPageWrapper.js"; + +/** + * 서버용 ProductDetailPage 렌더링 + * @param {Object} stores - { productStore, cartStore, uiStore } + * @returns {string} - 렌더링된 HTML + */ +export function renderProductDetailPage(stores) { + const { currentProduct: product, relatedProducts = [], error, loading } = stores.productStore.getState(); + + const loadingContent = ` +
+
+
+

상품 정보를 불러오는 중...

+
+
+ `; + + const ErrorContent = ({ error }) => ` +
+
+
+ + + +
+

상품을 찾을 수 없습니다

+

${error || "요청하신 상품이 존재하지 않습니다."}

+ + + 홈으로 + +
+
+ `; + + function ProductDetail({ product, relatedProducts = [] }) { + const { + productId, + title, + image, + lprice, + brand, + description = "", + rating = 0, + reviewCount = 0, + stock = 100, + category1, + category2, + } = product; + + const price = Number(lprice); + + const breadcrumbItems = []; + if (category1) breadcrumbItems.push({ name: category1, category: "category1", value: category1 }); + if (category2) breadcrumbItems.push({ name: category2, category: "category2", value: category2 }); + + return ` + ${ + breadcrumbItems.length > 0 + ? ` + + ` + : "" + } + +
+
+
+ ${title} +
+ +
+

${brand}

+

${title}

+ + ${ + rating > 0 + ? ` +
+
+ ${Array(5) + .fill(0) + .map( + (_, i) => ` + + + + `, + ) + .join("")} +
+ ${rating}.0 (${reviewCount.toLocaleString()}개 리뷰) +
+ ` + : "" + } + +
+ ${price.toLocaleString()}원 +
+ +
+ 재고 ${stock.toLocaleString()}개 +
+ + ${ + description + ? ` +
+ ${description} +
+ ` + : "" + } +
+
+ +
+
+ 수량 +
+ + + + + +
+
+ + +
+
+ +
+ +
+ + ${ + relatedProducts.length > 0 + ? ` +
+
+

관련 상품

+

같은 카테고리의 다른 상품들

+
+
+
+ ${relatedProducts + .slice(0, 20) + .map( + (relatedProduct) => ` + + `, + ) + .join("")} +
+
+
+ ` + : "" + } + `; + } + + const content = loading + ? loadingContent + : error && !product + ? ErrorContent({ error }) + : ProductDetail({ product, relatedProducts }); + + return ServerPageWrapper( + { + headerLeft: ` +
+ +

상품 상세

+
+ `.trim(), + children: content, + }, + stores, + ); +} diff --git a/packages/vanilla/src/pages/server/index.js b/packages/vanilla/src/pages/server/index.js new file mode 100644 index 00000000..277cb443 --- /dev/null +++ b/packages/vanilla/src/pages/server/index.js @@ -0,0 +1,4 @@ +export { ServerPageWrapper } from "./ServerPageWrapper.js"; +export { renderHomePage } from "./ServerHomePage.js"; +export { renderProductDetailPage } from "./ServerProductDetailPage.js"; +export { renderNotFoundPage } from "./ServerNotFoundPage.js"; diff --git a/packages/vanilla/src/stores/createServerStore.js b/packages/vanilla/src/stores/createServerStore.js new file mode 100644 index 00000000..3915c25a --- /dev/null +++ b/packages/vanilla/src/stores/createServerStore.js @@ -0,0 +1,80 @@ +import { PRODUCT_ACTIONS } from "./actionTypes.js"; + +/** + * 서버 사이드 렌더링용 Store 생성 + * 클라이언트의 createStore와 유사하지만 서버에서 동작하도록 단순화 + * @param {Object} initialState - 초기 상태 + * @returns {Object} - { getState, dispatch, subscribe } + */ +export function createServerStore(initialState) { + let state = { ...initialState }; + + return { + getState: () => state, + dispatch: (action) => { + switch (action.type) { + case PRODUCT_ACTIONS.SET_PRODUCTS: + state = { + ...state, + products: action.payload.products, + totalCount: action.payload.totalCount, + loading: false, + error: null, + }; + break; + case PRODUCT_ACTIONS.SET_CATEGORIES: + state = { + ...state, + categories: action.payload, + }; + break; + case PRODUCT_ACTIONS.SET_CURRENT_PRODUCT: + state = { + ...state, + currentProduct: action.payload, + loading: false, + error: null, + }; + break; + case PRODUCT_ACTIONS.SET_RELATED_PRODUCTS: + state = { + ...state, + relatedProducts: action.payload, + }; + break; + case PRODUCT_ACTIONS.SETUP: + state = { ...state, ...action.payload }; + break; + } + }, + subscribe: () => () => {}, // 서버에서는 구독 불필요 + }; +} + +/** + * SSR/SSG용 기본 Store들 초기화 + * @returns {Object} - { productStore, cartStore, uiStore } + */ +export function createServerStores() { + return { + productStore: createServerStore({ + products: [], + totalCount: 0, + currentProduct: null, + relatedProducts: [], + loading: false, + error: null, + status: "idle", + categories: {}, + }), + cartStore: createServerStore({ + items: [], + selectedAll: false, + }), + uiStore: createServerStore({ + cartModal: { isOpen: false }, + globalLoading: false, + toast: { isVisible: false, message: "", type: "info" }, + }), + }; +} diff --git a/packages/vanilla/src/utils/ssrUtils.js b/packages/vanilla/src/utils/ssrUtils.js new file mode 100644 index 00000000..cc45bd67 --- /dev/null +++ b/packages/vanilla/src/utils/ssrUtils.js @@ -0,0 +1,46 @@ +/** + * SSR/SSG 관련 유틸리티 함수들 + */ + +/** + * URL에서 쿼리 파라미터 파싱 + * @param {string} url - URL 문자열 + * @returns {{ pathname: string, query: Object }} + */ +export function parseUrl(url) { + const [pathname, queryString] = url.split("?"); + const query = {}; + + if (queryString) { + const params = new URLSearchParams(queryString); + for (const [key, value] of params) { + query[key] = value; + } + } + + return { pathname, query }; +} + +/** + * initialData 스크립트 태그 생성 + * @param {Object} initialData - 클라이언트로 전달할 초기 데이터 + * @returns {string} - script 태그 HTML + */ +export function createInitialDataScript(initialData) { + return ``; +} + +/** + * HTML 템플릿에 SSR 결과 삽입 + * @param {string} template - HTML 템플릿 + * @param {{ html: string, head: string, initialData: Object }} renderResult - 렌더링 결과 + * @returns {string} - 최종 HTML + */ +export function applyTemplate(template, { html, head, initialData }) { + const initialDataScript = createInitialDataScript(initialData); + + return template + .replace("", head) + .replace("", html) + .replace("", `${initialDataScript}`); +} diff --git a/packages/vanilla/static-site-generate.js b/packages/vanilla/static-site-generate.js index c479f112..ec9d5f4c 100644 --- a/packages/vanilla/static-site-generate.js +++ b/packages/vanilla/static-site-generate.js @@ -1,20 +1,155 @@ -import fs from "fs"; +import { readFileSync, writeFileSync, mkdirSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { createServer } from "vite"; +import { server as mswServer } from "./src/mocks/node.js"; -const render = () => { - return `
안녕하세요
`; -}; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const DIST_DIR = join(__dirname, "../../dist/vanilla"); +const TEMPLATE_PATH = join(__dirname, "../../dist/vanilla/index.html"); + +/** + * initialData 스크립트 태그 생성 + * @param {Object} initialData + * @returns {string} + */ +function createInitialDataScript(initialData) { + return ` + + `; +} + +/** + * HTML 템플릿에 SSR 결과 삽입 + * @param {string} template + * @param {{ html: string, head: string, initialData: Object }} renderResult + * @returns {string} + */ +function applyTemplate(template, { html, head, initialData }) { + const initialDataScript = createInitialDataScript(initialData); + + return template + .replace("", head) + .replace("", html) + .replace("", `${initialDataScript}`); +} + +/** + * 정적 사이트 생성 + */ async function generateStaticSite() { - // HTML 템플릿 읽기 - const template = fs.readFileSync("../../dist/vanilla/index.html", "utf-8"); + console.log("정적 사이트 생성 시작..."); + + let vite; + try { + // 1. MSW 서버 시작 + mswServer.listen({ + onUnhandledRequest: "bypass", + }); + console.log("MSW 서버 시작 완료"); + + // 2. Vite 서버 생성 + vite = await createServer({ + server: { middlewareMode: true }, + appType: "custom", + }); + + // 3. 렌더 함수 로드 (Vite를 통해 모든 import 처리) + const { render } = await vite.ssrLoadModule("./src/main-server.js"); + + // 4. 템플릿 로드 + const template = readFileSync(TEMPLATE_PATH, "utf-8"); + console.log("HTML 템플릿 로드 완료"); + + // 5. 페이지 목록 생성 + const pages = await getPages(vite); + console.log(`총 ${pages.length}개 페이지 생성 예정`); + + // 6. 각 페이지 렌더링 및 저장 + for (const page of pages) { + console.log(`페이지 생성 중: ${page.url}`); - // 어플리케이션 렌더링하기 - const appHtml = render(); + try { + const renderResult = await render(page.url, page.query || {}); + + // HTML 템플릿 적용 + const finalHtml = applyTemplate(template, renderResult); + + // 디렉토리 생성 (필요한 경우) + const dir = dirname(page.filePath); + mkdirSync(dir, { recursive: true }); + + // 파일 저장 + writeFileSync(page.filePath, finalHtml, "utf-8"); + console.log(`✓ ${page.filePath} 생성 완료`); + } catch (error) { + console.error(`페이지 생성 실패 (${page.url}):`, error); + } + } + + console.log("정적 사이트 생성 완료!"); + } catch (error) { + console.error("정적 사이트 생성 오류:", error); + process.exit(1); + } finally { + // Vite 서버와 MSW 서버 정리 + if (vite) { + await vite.close(); + } + mswServer.close(); + } +} + +/** + * 생성할 페이지 목록 반환 + */ +async function getPages(vite) { + const pages = [ + // 홈페이지 + { + url: "/", + filePath: join(DIST_DIR, "index.html"), + query: {}, + }, + // 404 페이지 + { + url: "/404", + filePath: join(DIST_DIR, "404.html"), + query: {}, + }, + ]; + + try { + // 상품 목록 조회 (동적 라우트용) - Vite를 통해 로드 + const { getProducts } = await vite.ssrLoadModule("./src/api/productApi.js"); + const productsResponse = await getProducts({ limit: 20, page: 1 }); + const products = productsResponse.products; + + // 상품 상세 페이지들 추가 + for (const product of products) { + pages.push({ + url: `/product/${product.productId}/`, + filePath: join(DIST_DIR, "product", product.productId, "index.html"), + query: {}, + }); + } + + console.log(`${products.length}개 상품 상세 페이지 추가`); + } catch (error) { + console.error("상품 목록 조회 실패:", error); + // 상품 목록 조회 실패해도 기본 페이지들은 생성 + } + + return pages; +} - // 결과 HTML 생성하기 - const result = template.replace("", appHtml); - fs.writeFileSync("../../dist/vanilla/index.html", result); +// 스크립트 직접 실행 시 +if (import.meta.url === `file://${process.argv[1]}`) { + generateStaticSite(); } -// 실행 -generateStaticSite(); +export { generateStaticSite }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 697cd2f9..0869ed0a 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 @@ -62,6 +62,9 @@ importers: lint-staged: specifier: ^15.2.11 version: 15.5.2 + nodemon: + specifier: ^3.1.11 + version: 3.1.11 prettier: specifier: ^3.4.2 version: 3.6.2 @@ -73,38 +76,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 +116,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 +152,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 +164,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 +183,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,7 +231,7 @@ 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: @@ -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,21 @@ 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/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 +785,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 +987,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 +1073,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 +1098,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 +1134,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 +1210,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 +1254,33 @@ 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@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -1309,9 +1331,9 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - ansis@4.1.0: - resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} - engines: {node: '>=14'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1327,12 +1349,8 @@ packages: 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 +1361,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -1366,10 +1388,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 +1403,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,9 +1415,9 @@ 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'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} @@ -1484,8 +1502,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 +1529,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==} @@ -1548,9 +1562,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 +1577,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'} @@ -1793,10 +1801,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'} @@ -1863,10 +1867,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'} @@ -1893,6 +1893,10 @@ packages: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1944,6 +1948,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1971,6 +1978,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2023,13 +2034,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 +2095,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 +2206,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==} @@ -2264,10 +2278,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'} @@ -2311,6 +2321,15 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + nodemon@3.1.11: + resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} + engines: {node: '>=10'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2322,6 +2341,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 +2390,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,10 +2413,6 @@ 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@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -2415,10 +2430,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 +2454,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 @@ -2481,6 +2492,9 @@ packages: psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2503,22 +2517,26 @@ packages: 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'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -2545,13 +2563,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 +2603,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 +2639,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==} @@ -2678,10 +2697,18 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + sirv@3.0.1: 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 +2736,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 +2750,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,13 +2774,14 @@ 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'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2773,34 +2797,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: @@ -2822,6 +2839,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -2879,6 +2900,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} @@ -2906,8 +2930,8 @@ 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 @@ -2915,11 +2939,6 @@ packages: 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 +2979,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 +3055,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 +3113,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,20 +3129,20 @@ 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 + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3141,6 +3157,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 +3186,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 +3201,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/core': 7.28.3 + '@babel/types': 7.28.5 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + dependencies: + '@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 +3249,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: @@ -3234,19 +3260,19 @@ snapshots: '@babel/parser': 7.28.3 '@babel/template': 7.27.2 '@babel/types': 7.28.2 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) 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 - debug: 4.4.1 + '@babel/types': 7.28.5 + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -3260,6 +3286,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 +3326,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 @@ -3396,7 +3427,7 @@ snapshots: '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -3414,7 +3445,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -3473,31 +3504,32 @@ 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/interceptors@0.39.2': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -3507,11 +3539,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 +3567,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 + '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.34': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.34': - optional: true + '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rolldown/pluginutils@1.0.0-beta.32': {} - - '@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 +3684,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 +3758,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 +3825,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': {} @@ -3825,7 +3862,7 @@ snapshots: '@typescript-eslint/types': 8.36.0 '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.36.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: @@ -3835,7 +3872,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) '@typescript-eslint/types': 8.36.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -3853,7 +3890,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) '@typescript-eslint/utils': 8.36.0(eslint@9.30.1)(typescript@5.8.3) - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) eslint: 9.30.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -3868,7 +3905,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) '@typescript-eslint/types': 8.36.0 '@typescript-eslint/visitor-keys': 8.36.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -3894,92 +3931,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@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) 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 +4021,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 +4040,10 @@ 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@2.0.0: dependencies: @@ -4053,7 +4085,10 @@ snapshots: ansi-styles@6.2.1: {} - ansis@4.1.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 argparse@2.0.1: {} @@ -4065,11 +4100,9 @@ snapshots: array-union@2.1.0: {} - assertion-error@2.0.1: {} - - 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,11 +4112,13 @@ snapshots: balanced-match@1.0.2: {} + binary-extensions@2.3.0: {} + body-parser@2.2.0: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -4115,8 +4150,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 +4164,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,7 +4173,17 @@ snapshots: chalk@5.4.1: {} - check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 cli-cursor@5.0.0: dependencies: @@ -4233,7 +4270,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: @@ -4244,14 +4281,14 @@ snapshots: dependencies: ms: 2.0.0 - debug@4.4.1: + debug@4.4.1(supports-color@5.5.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 decimal.js@10.6.0: {} - deep-eql@5.0.2: {} - deep-is@0.1.4: {} delayed-stream@1.0.0: {} @@ -4276,8 +4313,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 +4323,6 @@ snapshots: emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} entities@6.0.1: {} @@ -4397,7 +4430,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -4467,7 +4500,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -4539,7 +4572,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -4571,11 +4604,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 @@ -4646,15 +4674,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: {} @@ -4676,6 +4695,8 @@ snapshots: graphql@16.11.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -4707,14 +4728,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -4726,6 +4747,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ignore-by-default@1.0.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -4743,6 +4766,10 @@ snapshots: ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4780,22 +4807,16 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.29 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 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 +4906,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: {} @@ -4936,7 +4961,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 13.1.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.3 @@ -4984,14 +5009,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: @@ -5043,8 +5068,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minipass@7.1.2: {} - mrmime@2.0.1: {} ms@2.0.0: {} @@ -5088,6 +5111,21 @@ snapshots: node-releases@2.0.19: {} + nodemon@3.1.11: + dependencies: + chokidar: 3.6.0 + debug: 4.4.1(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.2 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + normalize-path@3.0.0: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -5096,6 +5134,8 @@ snapshots: object-inspect@1.13.4: {} + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5143,8 +5183,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,11 +5199,6 @@ snapshots: path-key@4.0.0: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - path-to-regexp@6.3.0: {} path-to-regexp@8.2.0: {} @@ -5176,8 +5209,6 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.1: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5192,11 +5223,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 @@ -5229,6 +5260,8 @@ snapshots: dependencies: punycode: 2.3.1 + pstree.remy@1.1.8: {} + punycode@2.3.1: {} qs@6.14.0: @@ -5248,16 +5281,20 @@ 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.2.3: {} - react@19.1.1: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 redent@3.0.0: dependencies: @@ -5279,40 +5316,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: @@ -5342,7 +5377,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -5370,7 +5405,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.26.0: {} + scheduler@0.27.0: {} semver@6.3.1: {} @@ -5378,7 +5413,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -5443,12 +5478,22 @@ snapshots: signal-exit@4.1.0: {} + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.2 + sirv@3.0.1: dependencies: '@polka/url': 1.0.0-next.29 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 +5514,7 @@ snapshots: statuses@2.0.2: {} - std-env@3.9.0: {} + std-env@3.10.0: {} strict-event-emitter@0.5.1: {} @@ -5481,12 +5526,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,14 +5548,14 @@ 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 + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5531,28 +5570,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: {} @@ -5568,6 +5602,8 @@ snapshots: totalist@3.0.1: {} + touch@3.1.1: {} + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -5621,6 +5657,8 @@ snapshots: typescript@5.8.3: {} + undefsafe@2.0.5: {} + undici-types@7.8.0: {} universalify@0.2.0: {} @@ -5644,34 +5682,13 @@ 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: {} - 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 - - 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 +5696,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 +5734,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 +5773,6 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml @@ -5808,12 +5817,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