sayren 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/boilerplate/.boilerplate-meta.json +44 -0
  2. package/dist/boilerplate/.claude/skills/sayren/SKILL.md +18 -0
  3. package/dist/boilerplate/.mcp.json +8 -0
  4. package/dist/boilerplate/CLAUDE.md +26 -0
  5. package/dist/boilerplate/README.md +12 -0
  6. package/dist/boilerplate/_gitignore +8 -0
  7. package/dist/boilerplate/biome.json +45 -0
  8. package/dist/boilerplate/messages/en.json +9 -0
  9. package/dist/boilerplate/messages/ko.json +9 -0
  10. package/dist/boilerplate/package.json +46 -0
  11. package/dist/boilerplate/public/_headers +5 -0
  12. package/dist/boilerplate/public/favicon.svg +1 -0
  13. package/dist/boilerplate/src/hooks/use-catalog.ts +28 -0
  14. package/dist/boilerplate/src/i18n/index.ts +99 -0
  15. package/dist/boilerplate/src/i18n/locale.server.ts +10 -0
  16. package/dist/boilerplate/src/lib/analytics.ts +60 -0
  17. package/dist/boilerplate/src/lib/api.server.ts +27 -0
  18. package/dist/boilerplate/src/lib/api.ts +7 -0
  19. package/dist/boilerplate/src/lib/config.server.ts +23 -0
  20. package/dist/boilerplate/src/lib/config.test.ts +16 -0
  21. package/dist/boilerplate/src/lib/config.ts +19 -0
  22. package/dist/boilerplate/src/lib/cookies.server.ts +35 -0
  23. package/dist/boilerplate/src/lib/format.ts +19 -0
  24. package/dist/boilerplate/src/lib/queries.ts +61 -0
  25. package/dist/boilerplate/src/routeTree.gen.ts +69 -0
  26. package/dist/boilerplate/src/router.tsx +44 -0
  27. package/dist/boilerplate/src/routes/__root.tsx +119 -0
  28. package/dist/boilerplate/src/routes/index.tsx +52 -0
  29. package/dist/boilerplate/src/start.ts +20 -0
  30. package/dist/boilerplate/src/styles.css +29 -0
  31. package/dist/boilerplate/src/theme.css +38 -0
  32. package/dist/boilerplate/tsconfig.json +16 -0
  33. package/dist/boilerplate/vite.config.ts +12 -0
  34. package/dist/boilerplate/vitest.config.ts +5 -0
  35. package/dist/index.mjs +353 -10
  36. package/package.json +6 -5
@@ -0,0 +1,61 @@
1
+ import { productSortSchema } from "@sayren/storefront-sdk";
2
+ import { queryOptions } from "@tanstack/react-query";
3
+ import { createServerFn } from "@tanstack/react-start";
4
+ import { z } from "zod";
5
+ import { apiFor } from "./api.server";
6
+
7
+ /**
8
+ * 기본 조회 — TanStack Query의 queryOptions 팩토리다. 라우트 loader(`ensureQueryData`)와 화면(`useSuspenseQuery`, `src/hooks`)이
9
+ * 같은 팩토리를 써서 queryKey가 갈라지지 않는다. 데이터는 서버 함수로 받는다(첫 요청은 서버가 받아 HTML에 싣는다).
10
+ * 금액·재고·주문 가능 여부는 응답 값을 그대로 보인다 — 화면에서 계산하지 않는다.
11
+ */
12
+
13
+ const getStore = createServerFn({ method: "GET" }).handler(() => apiFor().store.get());
14
+
15
+ const listCategories = createServerFn({ method: "GET" }).handler(() =>
16
+ apiFor().catalog.listCategories(),
17
+ );
18
+
19
+ export const productSearchSchema = z.object({
20
+ keyword: z.string().optional(),
21
+ categoryId: z.string().optional(),
22
+ sort: productSortSchema.optional(),
23
+ page: z.number().int().min(1).optional(),
24
+ size: z.number().int().min(1).max(60).optional(),
25
+ });
26
+ export type ProductSearch = z.infer<typeof productSearchSchema>;
27
+
28
+ const searchProducts = createServerFn({ method: "GET" })
29
+ .validator(productSearchSchema)
30
+ .handler(({ data }) =>
31
+ apiFor().catalog.searchProducts({ ...data, page: data.page ?? 1, size: data.size ?? 24 }),
32
+ );
33
+
34
+ const getProduct = createServerFn({ method: "GET" })
35
+ .validator(z.object({ productId: z.string().min(1) }))
36
+ .handler(({ data }) => apiFor().catalog.getProduct(data.productId));
37
+
38
+ /** 상점 이름·로고·연락처 — 상점 플랫폼 설정 › 상점 정보 값 */
39
+ export const storeQuery = () =>
40
+ queryOptions({ queryKey: ["store"], queryFn: () => getStore(), staleTime: 5 * 60_000 });
41
+
42
+ /** 카테고리는 자주 바뀌지 않는다 — 5분 동안 다시 받지 않는다 */
43
+ export const categoriesQuery = () =>
44
+ queryOptions({
45
+ queryKey: ["catalog", "categories"],
46
+ queryFn: () => listCategories(),
47
+ staleTime: 5 * 60_000,
48
+ });
49
+
50
+ export const productsQuery = (search: ProductSearch) =>
51
+ queryOptions({
52
+ queryKey: ["catalog", "products", search],
53
+ queryFn: () => searchProducts({ data: search }),
54
+ });
55
+
56
+ /** 상품 하나 — 상품 id 또는 상점별 상품번호 */
57
+ export const productQuery = (productId: string) =>
58
+ queryOptions({
59
+ queryKey: ["catalog", "product", productId],
60
+ queryFn: () => getProduct({ data: { productId } }),
61
+ });
@@ -0,0 +1,69 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as IndexRouteImport } from './routes/index'
13
+
14
+ const IndexRoute = IndexRouteImport.update({
15
+ id: '/',
16
+ path: '/',
17
+ getParentRoute: () => rootRouteImport,
18
+ } as any)
19
+
20
+ export interface FileRoutesByFullPath {
21
+ '/': typeof IndexRoute
22
+ }
23
+ export interface FileRoutesByTo {
24
+ '/': typeof IndexRoute
25
+ }
26
+ export interface FileRoutesById {
27
+ __root__: typeof rootRouteImport
28
+ '/': typeof IndexRoute
29
+ }
30
+ export interface FileRouteTypes {
31
+ fileRoutesByFullPath: FileRoutesByFullPath
32
+ fullPaths: '/'
33
+ fileRoutesByTo: FileRoutesByTo
34
+ to: '/'
35
+ id: '__root__' | '/'
36
+ fileRoutesById: FileRoutesById
37
+ }
38
+ export interface RootRouteChildren {
39
+ IndexRoute: typeof IndexRoute
40
+ }
41
+
42
+ declare module '@tanstack/react-router' {
43
+ interface FileRoutesByPath {
44
+ '/': {
45
+ id: '/'
46
+ path: '/'
47
+ fullPath: '/'
48
+ preLoaderRoute: typeof IndexRouteImport
49
+ parentRoute: typeof rootRouteImport
50
+ }
51
+ }
52
+ }
53
+
54
+ const rootRouteChildren: RootRouteChildren = {
55
+ IndexRoute: IndexRoute,
56
+ }
57
+ export const routeTree = rootRouteImport
58
+ ._addFileChildren(rootRouteChildren)
59
+ ._addFileTypes<FileRouteTypes>()
60
+
61
+ import type { getRouter } from './router.tsx'
62
+ import type { startInstance } from './start.ts'
63
+ declare module '@tanstack/react-start' {
64
+ interface Register {
65
+ ssr: true
66
+ router: Awaited<ReturnType<typeof getRouter>>
67
+ config: Awaited<ReturnType<typeof startInstance.getOptions>>
68
+ }
69
+ }
@@ -0,0 +1,44 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ import { createRouter } from "@tanstack/react-router";
3
+ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
4
+ import { routeTree } from "./routeTree.gen";
5
+
6
+ /** 라우트 loader가 받는 문맥 — loader가 `queryClient.ensureQueryData`로 미리 받아 둔다 */
7
+ export interface RouterContext {
8
+ queryClient: QueryClient;
9
+ }
10
+
11
+ export function getRouter() {
12
+ // 요청마다 새로 만든다 — 서버 렌더에서 다른 구매자의 캐시가 섞이지 않는다
13
+ const queryClient = new QueryClient({
14
+ defaultOptions: {
15
+ queries: {
16
+ // 서버가 받아 둔 데이터를 하이드레이션 직후 다시 부르지 않는다
17
+ staleTime: 30_000,
18
+ refetchOnWindowFocus: false,
19
+ retry: 1,
20
+ },
21
+ },
22
+ });
23
+
24
+ const router = createRouter({
25
+ routeTree,
26
+ context: { queryClient } satisfies RouterContext,
27
+ scrollRestoration: true,
28
+ // 링크에 마우스를 올리거나 포커스하면 다음 화면 데이터를 미리 받는다(30초 동안 다시 받지 않는다).
29
+ // loader가 상태를 바꾸는 화면(주문서 세션 생성·주문 완료의 바로구매 쿠키 삭제)은 라우트에서 `preload: false`로 뺀다
30
+ defaultPreload: "intent",
31
+ defaultPreloadStaleTime: 30_000,
32
+ });
33
+
34
+ // 서버에서 채운 쿼리 캐시를 HTML에 실어 보내고 브라우저에서 되살린다(QueryClientProvider도 여기서 감싼다)
35
+ setupRouterSsrQueryIntegration({ router, queryClient });
36
+
37
+ return router;
38
+ }
39
+
40
+ declare module "@tanstack/react-router" {
41
+ interface Register {
42
+ router: ReturnType<typeof getRouter>;
43
+ }
44
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ createRootRouteWithContext,
3
+ type ErrorComponentProps,
4
+ HeadContent,
5
+ Link,
6
+ Outlet,
7
+ Scripts,
8
+ } from "@tanstack/react-router";
9
+ import { createServerFn } from "@tanstack/react-start";
10
+ import { useEffect } from "react";
11
+ import { getLocale, m } from "../i18n";
12
+ import { type AnalyticsConfig, startAnalytics } from "../lib/analytics";
13
+ import { apiFor } from "../lib/api.server";
14
+ import { ANALYTICS_DEBUG, API_BASE_URL, resolveStoreCode } from "../lib/config.server";
15
+ import type { RouterContext } from "../router";
16
+ import appCss from "../styles.css?url";
17
+
18
+ /**
19
+ * 보일러플레이트의 루트 — 문서 머리, 방문 분석 시작, 오류 화면만 둔다. 헤더·푸터·메뉴는 쇼핑몰마다 새로 만든다.
20
+ * 화면 데이터는 `src/lib/queries.ts`의 queryOptions와 `src/hooks`의 훅으로 읽는다.
21
+ */
22
+
23
+ /** 방문 분석 설정 — 서버 값(`process.env`)이라 서버 함수로 내려준다 */
24
+ const getAnalyticsConfig = createServerFn({ method: "GET" }).handler(
25
+ (): AnalyticsConfig => ({
26
+ apiBaseUrl: API_BASE_URL,
27
+ storeCode: resolveStoreCode(),
28
+ debug: ANALYTICS_DEBUG,
29
+ }),
30
+ );
31
+
32
+ /** 상점 이름 — 문서 제목에 쓴다. 읽지 못해도 화면은 그린다 */
33
+ const getStoreName = createServerFn({ method: "GET" }).handler(async (): Promise<string | null> => {
34
+ const store = await apiFor()
35
+ .store.get()
36
+ .catch(() => null);
37
+ return store?.name ?? null;
38
+ });
39
+
40
+ export interface RootData {
41
+ analytics: AnalyticsConfig;
42
+ storeName: string | null;
43
+ }
44
+
45
+ export const Route = createRootRouteWithContext<RouterContext>()({
46
+ head: ({ loaderData }) => ({
47
+ meta: [
48
+ { charSet: "utf-8" },
49
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
50
+ { title: loaderData?.storeName ?? m.site_store_fallback() },
51
+ ],
52
+ links: [
53
+ // 본문 서체 Pretendard(theme.css의 --font-sans). 다른 서체는 여기에 https 스타일시트로 더한다
54
+ {
55
+ rel: "stylesheet",
56
+ href: "https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css",
57
+ },
58
+ { rel: "stylesheet", href: appCss },
59
+ { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
60
+ ],
61
+ }),
62
+ loader: async (): Promise<RootData> => {
63
+ const [analytics, storeName] = await Promise.all([getAnalyticsConfig(), getStoreName()]);
64
+ return { analytics, storeName };
65
+ },
66
+ staleTime: Number.POSITIVE_INFINITY,
67
+ shellComponent: RootDocument,
68
+ component: Root,
69
+ errorComponent: RootError,
70
+ notFoundComponent: NotFound,
71
+ });
72
+
73
+ function RootDocument({ children }: { children: React.ReactNode }) {
74
+ return (
75
+ <html lang={getLocale()}>
76
+ <head>
77
+ <HeadContent />
78
+ </head>
79
+ <body className="min-h-screen bg-page text-ink">
80
+ {children}
81
+ <Scripts />
82
+ </body>
83
+ </html>
84
+ );
85
+ }
86
+
87
+ function Root() {
88
+ const { analytics } = Route.useLoaderData();
89
+ // 방문 분석은 브라우저에서 한 번 시작한다. 이후 페이지뷰는 SDK가 History API 이동으로 센다
90
+ useEffect(() => {
91
+ startAnalytics(analytics);
92
+ }, [analytics]);
93
+ return <Outlet />;
94
+ }
95
+
96
+ function RootError({ error }: ErrorComponentProps) {
97
+ return (
98
+ <ErrorView
99
+ title={m.root_error_title()}
100
+ detail={error instanceof Error ? error.message : undefined}
101
+ />
102
+ );
103
+ }
104
+
105
+ function NotFound() {
106
+ return <ErrorView title={m.root_not_found_title()} />;
107
+ }
108
+
109
+ function ErrorView({ title, detail }: { title: string; detail?: string }) {
110
+ return (
111
+ <main className="space-y-3 px-4 py-16 text-center">
112
+ <h1 className="font-bold text-2xl">{title}</h1>
113
+ {detail ? <p className="text-muted">{detail}</p> : null}
114
+ <Link className="inline-block underline" to="/">
115
+ {m.root_go_home()}
116
+ </Link>
117
+ </main>
118
+ );
119
+ }
@@ -0,0 +1,52 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { useProducts, useStore } from "../hooks/use-catalog";
3
+ import { m } from "../i18n";
4
+ import { formatMoney } from "../lib/format";
5
+ import { productsQuery, storeQuery } from "../lib/queries";
6
+
7
+ /** 홈에 보이는 상품 수 — 첫 페이지만 받는다 */
8
+ const HOME_PRODUCTS = { page: 1, size: 24 } as const;
9
+
10
+ /** 보일러플레이트의 첫 화면 — 상점 이름과 상품 목록. 쇼핑몰을 만들면 이 파일을 홈으로 바꾼다 */
11
+ export const Route = createFileRoute("/")({
12
+ loader: ({ context }) =>
13
+ Promise.all([
14
+ context.queryClient.ensureQueryData(storeQuery()),
15
+ context.queryClient.ensureQueryData(productsQuery(HOME_PRODUCTS)),
16
+ ]),
17
+ component: Home,
18
+ });
19
+
20
+ function Home() {
21
+ const store = useStore();
22
+ const products = useProducts(HOME_PRODUCTS);
23
+ return (
24
+ <main className="mx-auto flex min-h-screen max-w-page flex-col gap-8 px-4 py-12">
25
+ <h1 className="font-bold text-3xl">{store.name}</h1>
26
+ {products.contents.length === 0 ? (
27
+ <p className="text-sub">{m.home_no_products()}</p>
28
+ ) : (
29
+ <ul className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 lg:grid-cols-4">
30
+ {products.contents.map((product) => (
31
+ <li key={product.productId} className="flex flex-col gap-2">
32
+ <div className="aspect-square overflow-hidden bg-chip">
33
+ {product.thumbnailUrl ? (
34
+ <img
35
+ src={product.thumbnailUrl}
36
+ alt={product.name}
37
+ loading="lazy"
38
+ className="h-full w-full object-cover"
39
+ />
40
+ ) : null}
41
+ </div>
42
+ <p className="text-sm">{product.name}</p>
43
+ <p className="font-bold text-sm">
44
+ {formatMoney(product.discountedPrice ?? product.salePrice, product.currency)}
45
+ </p>
46
+ </li>
47
+ ))}
48
+ </ul>
49
+ )}
50
+ </main>
51
+ );
52
+ }
@@ -0,0 +1,20 @@
1
+ import { createCsrfMiddleware, createMiddleware, createStart } from "@tanstack/react-start";
2
+ import { resolveLocale } from "./i18n";
3
+ import { runWithLocale } from "./i18n/locale.server";
4
+
5
+ /**
6
+ * 서버 함수 호출은 같은 사이트에서 온 것만 받는다(장바구니·결제 서버 함수를 더해도 다른 사이트가 대신 부르지 못하게).
7
+ * 구매자 로그인을 더하면 여기에 세션 갱신 미들웨어를 더한다(sdk-buyer-auth 스킬 참고).
8
+ */
9
+ const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
10
+
11
+ /** 요청 언어 — `?lang=` → `Accept-Language` → ko. 화면 렌더가 끝날 때까지 유지한다(`src/i18n`) */
12
+ const locale = createMiddleware().server(({ next, request }) =>
13
+ runWithLocale(resolveLocale(new URL(request.url), request.headers.get("accept-language")), () =>
14
+ next(),
15
+ ),
16
+ );
17
+
18
+ export const startInstance = createStart(() => ({
19
+ requestMiddleware: [locale, csrf],
20
+ }));
@@ -0,0 +1,29 @@
1
+ /* 클래스는 src/만 훑는다 — 빌드 산출물(dist)·설치물을 훑으면 빌드마다 CSS가 달라지고 서버·브라우저 빌드의 파일 이름이 어긋난다 */
2
+ @import "tailwindcss" source(".");
3
+ /* 상세설명이 에디터가 내보낸 HTML이라 그 모양 규칙이 필요하다 — 사진 줄, 재생기 틀,
4
+ 구분선. 활자는 product-description.tsx가 정하고 이 파일은 건드리지 않는다. */
5
+ @import "@avarlabs/editor/styles/reader.css";
6
+
7
+ /*
8
+ * 확장 지점 — 브랜드 색·서체·폭은 theme.css의 `@theme`에서 바꾼다. 화면 코드는 토큰만 쓰므로
9
+ * 그 파일만 고쳐도 전체 톤이 바뀐다.
10
+ */
11
+ @import "./theme.css";
12
+
13
+ html {
14
+ font-family: var(--font-sans);
15
+ word-break: keep-all;
16
+ }
17
+
18
+ body {
19
+ /* 모바일 가로 스크롤 방지 — 화면 밖으로 밀린 요소가 있어도 페이지 전체가 옆으로 밀리지 않는다.
20
+ 원인은 각 화면에서 min-w-0·break-words로 고친다. 이 규칙은 마지막 안전망이다 */
21
+ overflow-x: clip;
22
+ color: var(--color-ink);
23
+ -webkit-font-smoothing: antialiased;
24
+ }
25
+
26
+ /* 숫자(가격·수량·날짜)는 자릿수가 흔들리지 않게 한다 */
27
+ .tabular {
28
+ font-variant-numeric: tabular-nums;
29
+ }
@@ -0,0 +1,38 @@
1
+ /*
2
+ * 확장 지점 — 브랜드 색과 서체, 본문 폭은 여기서 바꾼다. 화면 코드는 토큰만 쓰므로
3
+ * 이 블록만 고쳐도 전체 톤이 바뀐다.
4
+ *
5
+ * - page: 페이지 바탕 · ink: 본문 글자·주요 버튼·판매가 · sub: 보조 본문 · muted: 캡션·정가
6
+ * - line: 구분선 · line-strong: 입력·보조 버튼 테두리 · chip: 옅은 면(모바일 섹션 띠·선택 행)
7
+ * - point: 할인율·필수 표시·오류 · point-soft: 혜택 배지 면 · info: 배송중 상태
8
+ * - container-page: 헤더와 본문의 최대 폭(`max-w-page`)
9
+ * - 글자 단차: caption 12 · meta 13 · body 14 · body-lg 15 (Tailwind 기본 xs·sm·base·lg·xl·2xl와 함께 쓴다)
10
+ *
11
+ * 기본 톤 — 흰 바탕·잉크 글자, 할인율에만 레드를 쓴다. 모서리 0
12
+ */
13
+ @theme {
14
+ --color-page: #ffffff;
15
+ --color-ink: #111111;
16
+ --color-sub: #555555;
17
+ --color-muted: #767676;
18
+ --color-line: #e5e5e5;
19
+ --color-line-strong: #d4d4d4;
20
+ --color-chip: #f5f5f5;
21
+ --color-point: #e8380d;
22
+ --color-point-soft: #fdf0eb;
23
+ --color-info: #2563eb;
24
+ --font-sans: "Pretendard Variable", Pretendard, -apple-system, "Apple SD Gothic Neo", sans-serif;
25
+ --color-surface: #f7f6f3;
26
+ --font-display: var(--font-sans);
27
+ --radius-card: 0;
28
+ --radius-control: 0;
29
+ --container-page: 80rem;
30
+ --text-caption: 0.75rem;
31
+ --text-caption--line-height: 1.5;
32
+ --text-meta: 0.8125rem;
33
+ --text-meta--line-height: 1.5;
34
+ --text-body: 0.875rem;
35
+ --text-body--line-height: 1.6;
36
+ --text-body-lg: 0.9375rem;
37
+ --text-body-lg--line-height: 1.6;
38
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "types": ["node", "vite/client"]
13
+ },
14
+ "include": ["**/*.ts", "**/*.tsx"],
15
+ "exclude": ["node_modules", "dist", ".output", ".tanstack"]
16
+ }
@@ -0,0 +1,12 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3
+ import viteReact from "@vitejs/plugin-react";
4
+ import { defineConfig, loadEnv } from "vite";
5
+
6
+ export default defineConfig(({ mode }) => {
7
+ // .env의 SAYREN_*를 서버 코드가 읽는 process.env에 싣는다. 셸에서 준 값이 우선한다.
8
+ // SAYREN_TOKEN(셀러 계정 토큰)은 싣지 않는다 — 사이트 코드가 셀러 토큰을 읽지 못하게
9
+ const { SAYREN_TOKEN: _cliToken, ...env } = loadEnv(mode, process.cwd(), "SAYREN_");
10
+ Object.assign(process.env, env, { ...process.env });
11
+ return { plugins: [tailwindcss(), tanstackStart(), viteReact()] };
12
+ });
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: { environment: "node", include: ["src/**/*.test.{ts,tsx}"] },
5
+ });