diff --git a/packages/vanilla/index.html b/packages/vanilla/index.html
index 483a6d5e..6af3e615 100644
--- a/packages/vanilla/index.html
+++ b/packages/vanilla/index.html
@@ -4,19 +4,20 @@
+
-
+
diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js
index b9a56d98..6602e680 100644
--- a/packages/vanilla/server.js
+++ b/packages/vanilla/server.js
@@ -1,31 +1,67 @@
import express from "express";
+import fs from "fs";
const prod = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5173;
-const base = process.env.BASE || (prod ? "/front_6th_chapter4-1/vanilla/" : "/");
+// const base = process.env.BASE || (prod ? "/front_6th_chapter4-1/vanilla/" : "/");
+const base = process.env.BASE || "/front_6th_chapter4-1/vanilla/";
const app = express();
-const render = () => {
- return `안녕하세요
`;
-};
-
-app.get("*all", (req, res) => {
- res.send(
- `
-
-
-
-
-
- Vanilla Javascript SSR
-
-
-${render()}
-
-
- `.trim(),
- );
+// Cached production assets
+const templateHtml = prod ? fs.readFileSync("./dist/vanilla/index.html", "utf-8") : "";
+
+let vite;
+
+if (!prod) {
+ const { createServer } = await import("vite");
+ vite = await createServer({
+ server: { middlewareMode: true },
+ appType: "custom",
+ base,
+ });
+ app.use(vite.middlewares);
+} else {
+ const compression = (await import("compression")).default;
+ const sirv = (await import("sirv")).default;
+ app.use(compression());
+ app.use(base, sirv("./dist/vanilla", { extensions: [] }));
+}
+
+app.use("*all", async (req, res) => {
+ try {
+ const url = req.originalUrl.replace(base, "");
+
+ /** @type {string} */
+ let template;
+ /** @type {import('./src/main-server.js').render} */
+ let render;
+ if (!prod) {
+ // Always read fresh template in development
+ template = fs.readFileSync("./index.html", "utf-8");
+ template = await vite.transformIndexHtml(url, template);
+ render = (await vite.ssrLoadModule("/src/main-server.js")).render;
+ } else {
+ template = templateHtml;
+ render = (await import("./dist/vanilla-ssr/main-server.js")).render;
+ }
+
+ const rendered = await render(url, req.query);
+
+ const html = template
+ .replace(``, rendered.head ?? "")
+ .replace(
+ ``,
+ ``,
+ )
+ .replace(``, rendered.html ?? "");
+
+ res.status(200).set({ "Content-Type": "text/html" }).send(html);
+ } catch (e) {
+ vite?.ssrFixStacktrace(e);
+ console.log(e.stack);
+ res.status(500).end(e.stack);
+ }
});
// Start http server
diff --git a/packages/vanilla/src/api/productApi.js b/packages/vanilla/src/api/productApi.js
index c2330fbe..00303d2e 100644
--- a/packages/vanilla/src/api/productApi.js
+++ b/packages/vanilla/src/api/productApi.js
@@ -1,3 +1,13 @@
+const prod = process.env.NODE_ENV === "production";
+
+const getBaseUrl = () => {
+ if (typeof window !== "undefined") {
+ return "";
+ }
+
+ return prod ? "http://localhost:4174" : "http://localhost:5174";
+};
+
export async function getProducts(params = {}) {
const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params;
const page = params.current ?? params.page ?? 1;
@@ -11,17 +21,17 @@ export async function getProducts(params = {}) {
sort,
});
- const response = await fetch(`/api/products?${searchParams}`);
+ const response = await fetch(`${getBaseUrl()}/api/products?${searchParams}`);
return await response.json();
}
export async function getProduct(productId) {
- const response = await fetch(`/api/products/${productId}`);
+ const response = await fetch(`${getBaseUrl()}/api/products/${productId}`);
return await response.json();
}
export async function getCategories() {
- const response = await fetch("/api/categories");
+ const response = await fetch(`${getBaseUrl()}/api/categories`);
return await response.json();
}
diff --git a/packages/vanilla/src/lib/Router.js b/packages/vanilla/src/lib/Router.js
index 2238a878..d053718e 100644
--- a/packages/vanilla/src/lib/Router.js
+++ b/packages/vanilla/src/lib/Router.js
@@ -14,6 +14,10 @@ export class Router {
this.#route = null;
this.#baseUrl = baseUrl.replace(/\/$/, "");
+ if (typeof window === "undefined") {
+ return;
+ }
+
window.addEventListener("popstate", () => {
this.#route = this.#findRoute();
this.#observer.notify();
@@ -25,7 +29,7 @@ export class Router {
}
get query() {
- return Router.parseQuery(window.location.search);
+ return Router.parseQuery(typeof window !== "undefined" ? window.location.search : {});
}
set query(newQuery) {
@@ -73,8 +77,8 @@ export class Router {
});
}
- #findRoute(url = window.location.pathname) {
- const { pathname } = new URL(url, window.location.origin);
+ #findRoute(url = typeof window !== "undefined" ? window.location.pathname : "/") {
+ const { pathname } = new URL(url, typeof window !== "undefined" ? window.location.origin : "/");
for (const [routePath, route] of this.#routes) {
const match = pathname.match(route.regex);
if (match) {
@@ -103,7 +107,9 @@ export class Router {
// baseUrl이 없으면 자동으로 붙여줌
let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url);
- const prevFullUrl = `${window.location.pathname}${window.location.search}`;
+ const prevFullUrl = `${typeof window !== "undefined" ? window.location.pathname : "/"}${
+ typeof window !== "undefined" ? window.location.search : ""
+ }`;
// 히스토리 업데이트
if (prevFullUrl !== fullUrl) {
@@ -130,7 +136,7 @@ export class Router {
* @param {string} search - location.search 또는 쿼리 문자열
* @returns {Object} 파싱된 쿼리 객체
*/
- static parseQuery = (search = window.location.search) => {
+ static parseQuery = (search = typeof window !== "undefined" ? window.location.search : "") => {
const params = new URLSearchParams(search);
const query = {};
for (const [key, value] of params) {
@@ -166,6 +172,8 @@ export class Router {
});
const queryString = Router.stringifyQuery(updatedQuery);
- return `${baseUrl}${window.location.pathname.replace(baseUrl, "")}${queryString ? `?${queryString}` : ""}`;
+ return `${baseUrl}${typeof window !== "undefined" ? window.location.pathname.replace(baseUrl, "") : "/"}${
+ queryString ? `?${queryString}` : ""
+ }`;
};
}
diff --git a/packages/vanilla/src/lib/ServerRouter.js b/packages/vanilla/src/lib/ServerRouter.js
new file mode 100644
index 00000000..912cda4e
--- /dev/null
+++ b/packages/vanilla/src/lib/ServerRouter.js
@@ -0,0 +1,158 @@
+/**
+ * Node.js 서버 라우터
+ */
+import { createObserver } from "./createObserver.js";
+
+export class ServerRouter {
+ #routes;
+ #route;
+ #observer = createObserver();
+ #baseUrl;
+ #currentQuery;
+
+ constructor(baseUrl = "") {
+ this.#routes = new Map();
+ this.#route = null;
+ this.#baseUrl = baseUrl.replace(/\/$/, "");
+ }
+
+ get baseUrl() {
+ return this.#baseUrl;
+ }
+
+ get query() {
+ return this.#currentQuery;
+ }
+
+ set query(newQuery) {
+ const newUrl = ServerRouter.getUrl(newQuery, this.#baseUrl);
+ this.push(newUrl);
+ }
+
+ get params() {
+ return this.#route?.params ?? {};
+ }
+
+ get route() {
+ return this.#route;
+ }
+
+ get target() {
+ return this.#route?.handler;
+ }
+
+ subscribe(fn) {
+ this.#observer.subscribe(fn);
+ }
+
+ /**
+ * 라우트 등록
+ * @param {string} path - 경로 패턴 (예: "/product/:id")
+ * @param {Function} handler - 라우트 핸들러
+ */
+ addRoute(path, handler) {
+ // 경로 패턴을 정규식으로 변환
+ const paramNames = [];
+ const regexPath = path
+ .replace(/:\w+/g, (match) => {
+ paramNames.push(match.slice(1)); // ':id' -> 'id'
+ return "([^/]+)";
+ })
+ .replace(/\//g, "\\/");
+
+ const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`);
+
+ this.#routes.set(path, {
+ regex,
+ paramNames,
+ handler,
+ });
+ }
+
+ #findRoute(url = "/", origin = "http://localhost") {
+ const { pathname } = new URL(url, origin);
+ for (const [routePath, route] of this.#routes) {
+ const match = pathname.match(route.regex);
+ if (match) {
+ // 매치된 파라미터들을 객체로 변환
+ const params = {};
+ route.paramNames.forEach((name, index) => {
+ params[name] = match[index + 1];
+ });
+
+ return {
+ ...route,
+ params,
+ path: routePath,
+ };
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 네비게이션 실행
+ * @param {string} url - 이동할 경로
+ */
+ push(url = "/") {
+ try {
+ this.#route = this.#findRoute(url);
+ } catch (error) {
+ console.error("라우터 네비게이션 오류:", error);
+ }
+ }
+
+ /**
+ * 라우터 시작
+ */
+ start(url = "/", query = {}) {
+ this.#currentQuery = query;
+ this.#route = this.#findRoute(url);
+ this.#observer.notify();
+ }
+
+ /**
+ * 쿼리 파라미터를 객체로 파싱
+ * @param {string} search - location.search 또는 쿼리 문자열
+ * @returns {Object} 파싱된 쿼리 객체
+ */
+ static parseQuery = (search = "") => {
+ const params = new URLSearchParams(search);
+ const query = {};
+ for (const [key, value] of params) {
+ query[key] = value;
+ }
+ return query;
+ };
+
+ /**
+ * 객체를 쿼리 문자열로 변환
+ * @param {Object} query - 쿼리 객체
+ * @returns {string} 쿼리 문자열
+ */
+ static stringifyQuery = (query) => {
+ const params = new URLSearchParams();
+ for (const [key, value] of Object.entries(query)) {
+ if (value !== null && value !== undefined && value !== "") {
+ params.set(key, String(value));
+ }
+ }
+ return params.toString();
+ };
+
+ static getUrl = (newQuery, baseUrl = "") => {
+ const currentQuery = ServerRouter.parseQuery();
+ const updatedQuery = { ...currentQuery, ...newQuery };
+
+ // 빈 값들 제거
+ Object.keys(updatedQuery).forEach((key) => {
+ if (updatedQuery[key] === null || updatedQuery[key] === undefined || updatedQuery[key] === "") {
+ delete updatedQuery[key];
+ }
+ });
+
+ const queryString = ServerRouter.stringifyQuery(updatedQuery);
+ //TODO: 수정해야하나?
+ return `${baseUrl}/${queryString ? `?${queryString}` : ""}`;
+ };
+}
diff --git a/packages/vanilla/src/lib/createStorage.js b/packages/vanilla/src/lib/createStorage.js
index 08b504f2..31e4d6fd 100644
--- a/packages/vanilla/src/lib/createStorage.js
+++ b/packages/vanilla/src/lib/createStorage.js
@@ -1,10 +1,29 @@
+const createMemoryStorage = () => {
+ let value = {};
+ return {
+ getItem: (key) => (key in value ? value[key] : null),
+ setItem: (key, value) => {
+ value[key] = value;
+ },
+ removeItem: (key) => {
+ delete value[key];
+ },
+ clear: () => {
+ value = {};
+ },
+ };
+};
+
/**
* 로컬스토리지 추상화 함수
* @param {string} key - 스토리지 키
* @param {Storage} storage - 기본값은 localStorage
* @returns {Object} { get, set, reset }
*/
-export const createStorage = (key, storage = window.localStorage) => {
+export const createStorage = (
+ key,
+ storage = typeof window !== "undefined" ? window.localStorage : createMemoryStorage(),
+) => {
const get = () => {
try {
const item = storage.getItem(key);
diff --git a/packages/vanilla/src/lib/index.js b/packages/vanilla/src/lib/index.js
index a598ef30..da63f1df 100644
--- a/packages/vanilla/src/lib/index.js
+++ b/packages/vanilla/src/lib/index.js
@@ -2,3 +2,4 @@ export * from "./createObserver";
export * from "./createStore";
export * from "./createStorage";
export * from "./Router";
+export * from "./ServerRouter";
diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js
index 40b58858..5c6d42ff 100644
--- a/packages/vanilla/src/main-server.js
+++ b/packages/vanilla/src/main-server.js
@@ -1,4 +1,136 @@
+import { HomePage, NotFoundPage, ProductDetailPage } from "./pages";
+import { router } from "./router";
+import { getProducts, getCategories, getProduct } from "./api/productApi.js";
+import { productStore } from "./stores";
+import { PRODUCT_ACTIONS } from "./stores/actionTypes";
+
+router.addRoute("/", HomePage);
+router.addRoute("/product/:id/", ProductDetailPage);
+router.addRoute(".*", NotFoundPage);
+
export const render = async (url, query) => {
- console.log({ url, query });
- return "";
+ router.start(url, query);
+ const route = router.route;
+ if (!route) {
+ return {
+ head: "",
+ html: NotFoundPage(),
+ initialData: JSON.stringify({}),
+ };
+ }
+ let head = "";
+ let data;
+
+ if (route.path === "/") {
+ try {
+ const [productsData, categories] = await Promise.all([getProducts(query), getCategories()]);
+
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SETUP,
+ payload: {
+ products: productsData,
+ categories: categories,
+ totalCount: productsData.pagination.total || 0,
+ loading: false,
+ status: "done",
+ error: null,
+ currentProduct: null,
+ relatedProducts: [],
+ },
+ });
+
+ head = `
+ 쇼핑몰 - 홈
+
+ `;
+ data = {
+ products: productsData.products || [],
+ categories: categories || {},
+ totalCount: productsData.pagination.total || 0,
+ };
+ } catch (error) {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SETUP,
+ payload: {
+ currentProduct: null,
+ relatedProducts: [],
+ loading: false,
+ status: "done",
+ error: error.message,
+ categories: {},
+ totalCount: 0,
+ products: [],
+ },
+ });
+
+ data = {
+ products: [],
+ categories: {},
+ totalCount: 0,
+ };
+ }
+ } else if (route.path === "/product/:id/") {
+ const productId = route.params.id;
+
+ try {
+ const product = await getProduct(productId);
+
+ let relatedProducts = [];
+ if (product && product.category2) {
+ relatedProducts = await getProducts({ category2: product.category2, limit: 20, page: 1 }).filter(
+ (p) => p.productId !== productId,
+ );
+ }
+
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SETUP,
+ payload: {
+ currentProduct: product,
+ relatedProducts: relatedProducts,
+ loading: false,
+ status: "done",
+ error: null,
+ categories: {},
+ totalCount: 0,
+ products: [],
+ },
+ });
+
+ head = `
+ ${product.title} - 쇼핑몰
+
+ `;
+
+ data = {
+ product: product,
+ relatedProducts: relatedProducts,
+ };
+ } catch (error) {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SETUP,
+ payload: {
+ currentProduct: null,
+ relatedProducts: [],
+ loading: false,
+ status: "done",
+ error: error.message,
+ categories: {},
+ totalCount: 0,
+ products: [],
+ },
+ });
+
+ data = {
+ products: [],
+ categories: {},
+ totalCount: 0,
+ };
+ }
+ }
+
+ return {
+ head: head,
+ html: router.target(),
+ initialData: JSON.stringify(data),
+ };
};
diff --git a/packages/vanilla/src/main.js b/packages/vanilla/src/main.js
index 4c3f2765..de910b25 100644
--- a/packages/vanilla/src/main.js
+++ b/packages/vanilla/src/main.js
@@ -4,6 +4,8 @@ import { registerAllEvents } from "./events";
import { loadCartFromStorage } from "./services";
import { router } from "./router";
import { BASE_URL } from "./constants.js";
+import { productStore } from "./stores";
+import { PRODUCT_ACTIONS } from "./stores/actionTypes";
const enableMocking = () =>
import("./mocks/browser.js").then(({ worker }) =>
@@ -15,10 +17,62 @@ const enableMocking = () =>
}),
);
+//hydraion SSR Data from server
+function hydrateFromSSRData() {
+ if (typeof window === "undefined" || !window.__INITIAL_DATA__) {
+ return;
+ }
+
+ try {
+ const initialData = window.__INITIAL_DATA__;
+
+ const currentPath = window.location.pathname;
+
+ // 홈페이지 hydration
+ if (currentPath === "/" && initialData.products) {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SETUP,
+ payload: {
+ products: initialData.products || [],
+ totalCount: initialData.totalCount || 0,
+ categories: initialData.categories || {},
+ currentProduct: null,
+ relatedProducts: [],
+ loading: false,
+ error: null,
+ status: "done",
+ },
+ });
+ }
+ // 상품 상세 페이지 hydration
+ else if (currentPath.includes("/product/") && initialData.product) {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SETUP,
+ payload: {
+ products: [],
+ totalCount: 0,
+ categories: {},
+ currentProduct: initialData.product,
+ relatedProducts: initialData.relatedProducts || [],
+ loading: false,
+ error: null,
+ status: "done",
+ },
+ });
+ }
+
+ // hydration 완료
+ window.__HYDRATED__ = true;
+ } catch (error) {
+ console.error(error);
+ }
+}
+
function main() {
registerAllEvents();
registerGlobalEvents();
loadCartFromStorage();
+ hydrateFromSSRData();
initRender();
router.start();
}
diff --git a/packages/vanilla/src/pages/HomePage.js b/packages/vanilla/src/pages/HomePage.js
index ca08c26c..5edc824a 100644
--- a/packages/vanilla/src/pages/HomePage.js
+++ b/packages/vanilla/src/pages/HomePage.js
@@ -7,7 +7,19 @@ import { PageWrapper } from "./PageWrapper.js";
export const HomePage = withLifecycle(
{
onMount: () => {
- loadProductsAndCategories();
+ if (typeof window !== "undefined") {
+ // Hydration된 데이터가 있으면 API 호출 스킵
+ if (window.__HYDRATED__) {
+ const { products, categories, status } = productStore.getState();
+ if (products.length > 0 && Object.keys(categories).length > 0 && status === "done") {
+ console.log("✅ Skip loading - data already hydrated from SSR");
+ return;
+ }
+ }
+
+ console.log("🔄 Loading data from client");
+ loadProductsAndCategories();
+ }
},
watches: [
() => {
diff --git a/packages/vanilla/src/pages/ProductDetailPage.js b/packages/vanilla/src/pages/ProductDetailPage.js
index 73d0ec30..0bd552dc 100644
--- a/packages/vanilla/src/pages/ProductDetailPage.js
+++ b/packages/vanilla/src/pages/ProductDetailPage.js
@@ -237,7 +237,17 @@ function ProductDetail({ product, relatedProducts = [] }) {
export const ProductDetailPage = withLifecycle(
{
onMount: () => {
- loadProductDetailForPage(router.params.id);
+ if (typeof window !== "undefined") {
+ if (window.__HYDRATED__) {
+ const { currentProduct, status } = productStore.getState();
+ const productId = router.params.id;
+ if (currentProduct && currentProduct.productId === productId && status === "done") {
+ return;
+ }
+ }
+
+ loadProductDetailForPage(router.params.id);
+ }
},
watches: [() => [router.params.id], () => loadProductDetailForPage(router.params.id)],
},
diff --git a/packages/vanilla/src/router/router.js b/packages/vanilla/src/router/router.js
index d897ee76..75e7ba3c 100644
--- a/packages/vanilla/src/router/router.js
+++ b/packages/vanilla/src/router/router.js
@@ -1,5 +1,5 @@
// 글로벌 라우터 인스턴스
-import { Router } from "../lib";
+import { Router, ServerRouter } from "../lib";
import { BASE_URL } from "../constants.js";
-export const router = new Router(BASE_URL);
+export const router = typeof window !== "undefined" ? new Router(BASE_URL) : new ServerRouter(BASE_URL);
diff --git a/packages/vanilla/src/router/withLifecycle.js b/packages/vanilla/src/router/withLifecycle.js
index ccb21113..a179c787 100644
--- a/packages/vanilla/src/router/withLifecycle.js
+++ b/packages/vanilla/src/router/withLifecycle.js
@@ -31,10 +31,12 @@ const mount = (page) => {
const lifecycle = getPageLifecycle(page);
if (lifecycle.mounted) return;
- // 마운트 콜백들 실행
- lifecycle.mount?.();
- lifecycle.mounted = true;
- lifecycle.deps = [];
+ // 마운트 콜백들 실행: 서버사이드에서는 돔 추가 및 제거 시 이벤트 발생X
+ if (typeof window !== "undefined") {
+ lifecycle.mount?.();
+ lifecycle.mounted = true;
+ lifecycle.deps = [];
+ }
};
// 페이지 언마운트 처리
@@ -43,9 +45,11 @@ const unmount = (pageFunction) => {
if (!lifecycle.mounted) return;
- // 언마운트 콜백들 실행
- lifecycle.unmount?.();
- lifecycle.mounted = false;
+ // 언마운트 콜백들 실행: 서버사이드에서는 돔 추가 및 제거 시 이벤트 발생X
+ if (typeof window !== "undefined") {
+ lifecycle.unmount?.();
+ lifecycle.mounted = false;
+ }
};
export const withLifecycle = ({ onMount, onUnmount, watches } = {}, page) => {
diff --git a/packages/vanilla/static-site-generate.js b/packages/vanilla/static-site-generate.js
index c479f112..37fcc8e1 100644
--- a/packages/vanilla/static-site-generate.js
+++ b/packages/vanilla/static-site-generate.js
@@ -1,19 +1,20 @@
import fs from "fs";
-
-const render = () => {
- return `안녕하세요
`;
-};
+import { render } from "./dist/vanilla-ssr/main-server.js";
async function generateStaticSite() {
- // HTML 템플릿 읽기
+ // 임시. HTML 템플릿 읽기
const template = fs.readFileSync("../../dist/vanilla/index.html", "utf-8");
- // 어플리케이션 렌더링하기
- const appHtml = render();
+ const rendered = await render("/", {});
+ const html = template
+ .replace(``, rendered.head ?? "")
+ .replace(
+ ``,
+ ``,
+ )
+ .replace(``, rendered.html ?? "");
- // 결과 HTML 생성하기
- const result = template.replace("", appHtml);
- fs.writeFileSync("../../dist/vanilla/index.html", result);
+ fs.writeFileSync("../../dist/vanilla/index.html", html);
}
// 실행
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 766adcbc..fff88855 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,7 +101,7 @@ importers:
version: 6.8.0
'@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.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@testing-library/user-event':
specifier: latest
version: 14.6.1(@testing-library/dom@10.4.1)
@@ -110,10 +110,10 @@ importers:
version: 24.0.13
'@types/react':
specifier: latest
- version: 19.1.11
+ version: 19.1.12
'@types/react-dom':
specifier: latest
- version: 19.1.7(@types/react@19.1.11)
+ version: 19.1.9(@types/react@19.1.12)
'@types/use-sync-external-store':
specifier: latest
version: 1.5.0
@@ -177,16 +177,16 @@ importers:
version: 24.0.13
'@types/react':
specifier: latest
- version: 19.1.11
+ version: 19.1.12
'@types/react-dom':
specifier: latest
- version: 19.1.7(@types/react@19.1.11)
+ version: 19.1.9(@types/react@19.1.12)
'@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.0.2(rolldown-vite@7.1.5(@types/node@24.0.13)(yaml@2.8.0))
compression:
specifier: ^1.7.5
version: 1.8.1
@@ -1103,13 +1103,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.1.9':
+ resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
peerDependencies:
'@types/react': ^19.0.0
- '@types/react@19.1.11':
- resolution: {integrity: sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==}
+ '@types/react@19.1.12':
+ resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==}
'@types/statuses@2.0.6':
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
@@ -1191,8 +1191,8 @@ packages:
peerDependencies:
vite: ^4 || ^5 || ^6 || ^7
- '@vitejs/plugin-react@5.0.1':
- resolution: {integrity: sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==}
+ '@vitejs/plugin-react@5.0.2':
+ resolution: {integrity: sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
@@ -3701,15 +3701,15 @@ 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/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
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)
optionalDependencies:
- '@types/react': 19.1.11
- '@types/react-dom': 19.1.7(@types/react@19.1.11)
+ '@types/react': 19.1.12
+ '@types/react-dom': 19.1.9(@types/react@19.1.12)
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
dependencies:
@@ -3759,11 +3759,11 @@ snapshots:
dependencies:
undici-types: 7.8.0
- '@types/react-dom@19.1.7(@types/react@19.1.11)':
+ '@types/react-dom@19.1.9(@types/react@19.1.12)':
dependencies:
- '@types/react': 19.1.11
+ '@types/react': 19.1.12
- '@types/react@19.1.11':
+ '@types/react@19.1.12':
dependencies:
csstype: 3.1.3
@@ -3878,12 +3878,12 @@ snapshots:
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.0.2(rolldown-vite@7.1.5(@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
+ '@rolldown/pluginutils': 1.0.0-beta.34
'@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)
@@ -3905,7 +3905,7 @@ snapshots:
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)
+ 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)
transitivePeerDependencies:
- supports-color