From 93583726570b682c4392a7d7d2d31fff0e009e02 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:48:30 +0800 Subject: [PATCH] feat(portal-shell): add MSW mock layer with production bundle exclusion (P1-5) MSW v2.7.0 fallback layer covering dashboard/users/exams/grades domains. NEXT_PUBLIC_MSW=1 enables browser Service Worker + SSR route handler mock responses without backend. Production build excludes all mock data via Turbopack resolveAlias redirecting @/mocks to empty stub. Acceptance: build bundle (client+server) verified clean of mock strings; typecheck/lint/vitest (231 tests) all pass. --- apps/portal-shell/ARCHITECTURE.md | 23 +- apps/portal-shell/next-env.d.ts | 2 +- apps/portal-shell/next.config.js | 40 +- apps/portal-shell/package.json | 8 +- apps/portal-shell/public/mockServiceWorker.js | 307 ++++++++++++ .../portal-shell/src/app/api/graphql/route.ts | 24 +- apps/portal-shell/src/lib/apollo-client.ts | 6 +- apps/portal-shell/src/mocks/browser.ts | 13 + apps/portal-shell/src/mocks/empty.ts | 32 ++ apps/portal-shell/src/mocks/graphql-data.ts | 437 ++++++++++++++++++ apps/portal-shell/src/mocks/handlers.ts | 63 +++ apps/portal-shell/src/mocks/index.ts | 30 ++ apps/portal-shell/src/mocks/server.ts | 13 + .../src/providers/MswProvider.tsx | 42 ++ apps/portal-shell/src/shell/ClientShell.tsx | 51 +- pnpm-lock.yaml | 268 +++++++++-- 16 files changed, 1292 insertions(+), 67 deletions(-) create mode 100644 apps/portal-shell/public/mockServiceWorker.js create mode 100644 apps/portal-shell/src/mocks/browser.ts create mode 100644 apps/portal-shell/src/mocks/empty.ts create mode 100644 apps/portal-shell/src/mocks/graphql-data.ts create mode 100644 apps/portal-shell/src/mocks/handlers.ts create mode 100644 apps/portal-shell/src/mocks/index.ts create mode 100644 apps/portal-shell/src/mocks/server.ts create mode 100644 apps/portal-shell/src/providers/MswProvider.tsx diff --git a/apps/portal-shell/ARCHITECTURE.md b/apps/portal-shell/ARCHITECTURE.md index cec4e4c..28d3350 100644 --- a/apps/portal-shell/ARCHITECTURE.md +++ b/apps/portal-shell/ARCHITECTURE.md @@ -757,7 +757,7 @@ export default async function ExamsPage(): Promise { - **P0-7**:`eslint .` → 0 errors, 2 warnings(均在 `__generated__/types.ts` 生成文件);[eslint.config.js](file:///e:/Desktop/Edu/apps/portal-shell/eslint.config.js) 含 `no-restricted-imports` - **P0-8**:`git status` 干净(commit `cfb7b00`);`.env.local` 与 `tsconfig.tsbuildinfo` 已在 [.gitignore](file:///e:/Desktop/Edu/apps/portal-shell/.gitignore) -### P1 · 框架与数据源接通(1–2 周)— 进行中(P1-1/P1-2/P1-3/P1-4 ✅ 2026-07-22 验收) +### P1 · 框架与数据源接通(1–2 周)— 进行中(P1-1/P1-2/P1-3/P1-4/P1-5 ✅ 2026-07-22 验收) **目标**:AppFrame + 导航 + 真实数据仪表盘 + 页面模板 + MSW + i18n,页面迁移的"流水线"建成。 @@ -767,7 +767,7 @@ export default async function ExamsPage(): Promise { | P1-2 | 仪表盘改接真实查询:`teacherDashboard`/`studentDashboard`/`parentDashboard`/`adminDashboard` + `warnings`/`errorBookStats` | 起全栈,4 角色仪表盘显示真实聚合数据(截图);widget 假契约查询(grades/homeworks/schedule/attendance/exams/announcements)全部下线或改造 | ✅ | | P1-3 | 页面模板四件套(list/detail/form/workbench)+ 三态规范 | Storybook 或示例页 4 张(/shell/dev/templates/*,仅 dev 可见;注:Next.js 私有文件夹 `_xxx` 不参与路由,故使用 `dev` 而非 `_dev`) | ✅ | | P1-4 | next-intl 接入 + messages 合并迁移 | 切换 locale 页面文案切换(截图);`useT` 自造函数删除 | ✅ | -| P1-5 | MSW 兜底层(迁移旧 handlers,覆盖 dashboard/users/exams/grades 四域起步) | `NEXT_PUBLIC_MSW=1` 无后端启动,仪表盘 + users 页有数据(截图);生产构建 bundle 无 mocks | ⏳ | +| P1-5 | MSW 兜底层(迁移旧 handlers,覆盖 dashboard/users/exams/grades 四域起步) | `NEXT_PUBLIC_MSW=1` 无后端启动,仪表盘 + users 页有数据(截图);生产构建 bundle 无 mocks | ✅ | | P1-6 | 31 widget 令牌清债(271 处机械替换)+ `border border` 去重 + 未知类检测进 arch:scan | `grep -c "text-heading-\|mt-sm\|py-xs\|p-md" src/widgets` = 0;`pnpm lint:tokens` 通过 | ⏳ | | P1-7 | codegen 恢复 typescript-operations(config + data-ana 两域先行关闭 skipDocumentsValidation) | 生成操作级类型;lib/api 对应域删除手写 interface;typecheck 通过 | ⏳ | | P1-8 | CI 增补:路由表一致性脚本 + 页面计数 + codegen diff 检查 | CI 对预埋违规报红(附 pipeline 链接) | ⏳ | @@ -844,6 +844,25 @@ export default async function ExamsPage(): Promise { - `GET /login`(`Cookie: NEXT_LOCALE=en`)→ `` + `NextIntlClientProvider locale="en"` + 英文文案("Save"/"Cancel"/"Toggle sidebar") - **质量校验**:`tsc --noEmit` 通过;`eslint src` → 0 errors, 2 warnings(`__generated__/types.ts` 生成文件);`vitest run` 全量 21 test files / 231 tests 全部通过 +**P1-5 验收证据(2026-07-22)**: + +- **MSW v2.7.0 兜底层(覆盖 dashboard/users/exams/grades 四域起步)**: + - [src/mocks/index.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/index.ts):`initMocks()` 入口,`NEXT_PUBLIC_MSW=1` 时按环境动态启动 browser worker 或 node server + - [src/mocks/handlers.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/handlers.ts):拦截 `POST /api/graphql` 与 `POST {APOLLO_ROUTER_URL}`,按 `operationName` 路由 + - [src/mocks/graphql-data.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/graphql-data.ts):mock 数据与 `graphqlResponse()` 函数,供 handlers.ts(MSW browser)和 route.ts(SSR)共用 + - [src/mocks/browser.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/browser.ts) / [server.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/server.ts):`setupWorker` / `setupServer` + - [public/mockServiceWorker.js](file:///e:/Desktop/Edu/apps/portal-shell/public/mockServiceWorker.js):MSW Service Worker(`pnpm exec msw init public/ --save`) +- **SSR 端 mock 数据(Apollo Client 走同域代理)**: + - [src/lib/apollo-client.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/apollo-client.ts):`MSW_ENABLED` 时 SSR 端 HttpLink 也指向 `/api/graphql`(不走直连 router) + - [src/app/api/graphql/route.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/app/api/graphql/route.ts):`MSW_ENABLED` 时直接返回 mock 数据,不连接后端 +- **生产构建安全(bundle 无 mocks)**: + - [src/mocks/empty.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/empty.ts):空 stub,导出与 `index.ts`/`graphql-data.ts` 同签名的 no-op 函数 + - [next.config.js](file:///e:/Desktop/Edu/apps/portal-shell/next.config.js):`NEXT_PUBLIC_MSW!=1` 时 Turbopack `resolveAlias` + webpack `resolve.alias` 把 `@/mocks` 与 `@/mocks/graphql-data` 重定向到 `@/mocks/empty`(注:Turbopack 不支持 Windows 绝对路径,故使用 `@/` 说明符) + - [src/providers/MswProvider.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/providers/MswProvider.tsx):静态 `import { initMocks } from "@/mocks"`(alias 生效后指向 empty.ts),`MSW_ENABLED=false` 时 useEffect 分支被 dead-code 消除 + - **构建验证**:`NEXT_PUBLIC_MSW=0 next build` 成功(20 路由生成);`Select-String -Path ".next/static/chunks/**/*.js",".next/server/**/*.js" -Pattern "张老师","stu-001","setupWorker","dev-teacher-001","二次函数"` → **CLEAN:客户端 + 服务端 bundle 均无 mock 字符串** +- **dev server 验证(MSW=1 无后端)**:`GET /api/graphql` 返回 `{msw:true}`;`POST /api/graphql` 返回 mock 数据(teacherDashboard: total_classes=5/total_students=142;users: 5 条记录;questions: 3 条;grades: 3 条) +- **质量校验**:`tsc --noEmit` 通过;`eslint src` → 0 errors, 2 warnings(`__generated__/types.ts`);`vitest run` 全量 21 test files / 231 tests 全部通过 + ### P2 · 教师域页面(2–3 周,可与 P3 部分并行) - 范围:§9.1 全表(~50 页)。顺序建议:exams → homework → grades → lesson-plans → questions/textbooks → attendance/classes/students → diagnostic/error-book/analytics → elective/course-plans → ai-* → practice/schedule-changes/leave。 diff --git a/apps/portal-shell/next-env.d.ts b/apps/portal-shell/next-env.d.ts index c4b7818..9edff1c 100644 --- a/apps/portal-shell/next-env.d.ts +++ b/apps/portal-shell/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/portal-shell/next.config.js b/apps/portal-shell/next.config.js index 1f63cd9..fcf7465 100644 --- a/apps/portal-shell/next.config.js +++ b/apps/portal-shell/next.config.js @@ -1,5 +1,5 @@ /** - * Next.js 配置(v2.1 M8 + v0.2 Tailwind v4 + Next 16 Turbopack + P1-4 next-intl) + * Next.js 配置(v2.1 M8 + v0.2 Tailwind v4 + Next 16 Turbopack + P1-4 next-intl + P1-5 MSW) * * 角色:插件化仪表盘宿主(单 Next.js App Router · 单 Docker) * - output:standalone(单容器部署) @@ -7,17 +7,32 @@ * - 反向代理:/api/v1/* → api-gateway :8080(JWT 校验 + 注入 x-user-id/x-user-role) * - GraphQL 查询走 apollo-router :3000(M8 验收点,由 Apollo Client 直连) * - next-intl:无 i18n 路由模式(locale 由 cookie 决定,ARCHITECTURE.md §3.4 V3-A6) + * - MSW 兜底层(P1-5):NEXT_PUBLIC_MSW!=1 时把 @/mocks 与 @/mocks/graphql-data + * 重定向到空 stub,确保生产 bundle 不含 mock 数据(ARCHITECTURE.md §3.4 V3-A7) * * Next 16 默认 Turbopack: * - turbopack.resolveExtensions 处理 ESM 包 .js 后缀导入源码 TS 文件的映射 * - webpack 配置保留作为 fallback(--webpack flag 时生效) * - * 关联:portal-shell ARCHITECTURE.md §3.4 V3-A6、spec §2、project_rules §3.2 + * 关联:portal-shell ARCHITECTURE.md §3.4 V3-A6/V3-A7、spec §2、project_rules §3.2 */ +import path from "node:path"; +import { fileURLToPath } from "node:url"; import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// P1-5:MSW 启用开关。生产构建(NEXT_PUBLIC_MSW 非等于 "1")时把 +// @/mocks 与 @/mocks/graphql-data 重定向到空 stub, +// 确保 bundle 不含真实 mock 数据与 msw 包。 +// 注意:Turbopack resolveAlias 不支持 Windows 绝对路径("windows imports +// are not implemented yet"),故使用 @/mocks/empty 说明符(由 tsconfig +// paths 解析为 src/mocks/empty.ts),而非绝对路径。 +const MSW_ENABLED = process.env.NEXT_PUBLIC_MSW === "1"; +const MOCKS_STUB_SPECIFIER = "@/mocks/empty"; + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, @@ -43,6 +58,17 @@ const nextConfig = { ".cjs", ".json", ], + // P1-5:MSW 关闭时把 @/mocks(MswProvider 静态 import)与 + // @/mocks/graphql-data(route.ts 静态 import)重定向到空 stub。 + // 使用 @/mocks/empty 说明符作为目标(Turbopack 不支持 Windows 绝对路径)。 + ...(MSW_ENABLED + ? {} + : { + resolveAlias: { + "@/mocks": MOCKS_STUB_SPECIFIER, + "@/mocks/graphql-data": MOCKS_STUB_SPECIFIER, + }, + }), }, // Webpack 配置(fallback,使用 --webpack flag 时生效) webpack(config) { @@ -51,6 +77,16 @@ const nextConfig = { ...config.resolve.extensionAlias, ".js": [".ts", ".tsx", ".js"], }; + // P1-5:MSW 关闭时把 @/mocks 与 @/mocks/graphql-data 重定向到空 stub。 + // webpack resolve.alias 支持 exact 匹配($ 后缀)与前缀匹配, + // 这里用 exact 匹配避免误伤其他 @/mocks/xxx 路径(实际只有这两个 import 点)。 + if (!MSW_ENABLED) { + config.resolve.alias = { + ...config.resolve.alias, + "@/mocks$": MOCKS_STUB_SPECIFIER, + "@/mocks/graphql-data$": MOCKS_STUB_SPECIFIER, + }; + } return config; }, async rewrites() { diff --git a/apps/portal-shell/package.json b/apps/portal-shell/package.json index 7968a04..a5129b3 100644 --- a/apps/portal-shell/package.json +++ b/apps/portal-shell/package.json @@ -38,6 +38,7 @@ "graphql": "^16.8.0", "jose": "^5.9.6", "lucide-react": "^0.562.0", + "msw": "2.7.0", "next": "^16.0.10", "next-intl": "^4.13.2", "next-themes": "^0.4.6", @@ -69,5 +70,10 @@ "tsx": "^4.0.0", "typescript": "^5.6.0", "vitest": "^2.0.0" + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/apps/portal-shell/public/mockServiceWorker.js b/apps/portal-shell/public/mockServiceWorker.js new file mode 100644 index 0000000..ec47a9a --- /dev/null +++ b/apps/portal-shell/public/mockServiceWorker.js @@ -0,0 +1,307 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.7.0' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/apps/portal-shell/src/app/api/graphql/route.ts b/apps/portal-shell/src/app/api/graphql/route.ts index d938b63..98520fc 100644 --- a/apps/portal-shell/src/app/api/graphql/route.ts +++ b/apps/portal-shell/src/app/api/graphql/route.ts @@ -1,9 +1,15 @@ /** - * GraphQL 同域代理(P0-3,ARCHITECTURE.md §3.4 V3-A2 / §4 / §5.2) + * GraphQL 同域代理(P0-3 + P1-5,ARCHITECTURE.md §3.4 V3-A2/V3-A7 / §4 / §5.2 / §10 P1-5) * * 浏览器 Apollo Client 一律走同域 `/api/graphql`: * Browser → /api/graphql (本 Route Handler) → apollo-router :3000 * + * MSW 兜底层(P1-5): + * - NEXT_PUBLIC_MSW=1 时,SSR 端 Apollo Client 也走 /api/graphql(见 apollo-client.ts) + * - 本 Route Handler 检测 MSW 开关,开启时直接返回 mock 数据(不连接后端) + * - Mock 数据来自 mocks/graphql-data.ts,与 MSW browser worker 共用 + * - 生产构建 NEXT_PUBLIC_MSW 不为 "1",此分支被 tree-shake 移除 + * * 职责: * 1. 从 httpOnly cookie `edu_session` 取 JWT,注入 `Authorization: Bearer` * 2. 透传 body(APQ hash 或 query)与 Apollo 相关头 @@ -16,10 +22,13 @@ * * 验收命令: * DevTools Network 面板无 `localhost:3000` 直连;所有 GraphQL 请求走 `/api/graphql` + * NEXT_PUBLIC_MSW=1 pnpm dev → 仪表盘/users 有数据,无需后端 */ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import { graphqlResponse } from "@/mocks/graphql-data"; + export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -30,6 +39,8 @@ const UPSTREAM_URL = process.env.NEXT_PUBLIC_APOLLO_ROUTER_URL || "http://localhost:3000/graphql"; +const MSW_ENABLED = process.env.NEXT_PUBLIC_MSW === "1"; + /** * 从 Cookie 头解析指定 cookie 值。 */ @@ -44,6 +55,16 @@ function readCookie(cookieHeader: string | null, name: string): string | null { } export async function POST(req: NextRequest): Promise { + // P1-5 MSW 兜底层:开启时直接返回 mock 数据,不连接后端 + if (MSW_ENABLED) { + const body = (await req.json().catch(() => ({}))) as { + operationName?: string; + }; + return NextResponse.json(graphqlResponse(body.operationName), { + headers: { "Cache-Control": "no-store" }, + }); + } + const cookieHeader = req.headers.get("cookie"); const token = readCookie(cookieHeader, SESSION_COOKIE); @@ -117,6 +138,7 @@ export async function GET(): Promise { ok: true, proxy: "/api/graphql", upstream: UPSTREAM_URL, + msw: MSW_ENABLED, }, { status: 200 }, ); diff --git a/apps/portal-shell/src/lib/apollo-client.ts b/apps/portal-shell/src/lib/apollo-client.ts index 5004ddb..8058f09 100644 --- a/apps/portal-shell/src/lib/apollo-client.ts +++ b/apps/portal-shell/src/lib/apollo-client.ts @@ -39,6 +39,9 @@ const CLIENT_PROXY_URL = "/api/graphql"; // 生产环境默认启用(未设置或设置为 true 均启用) const APQ_ENABLED = process.env.NEXT_PUBLIC_APOLLO_APQ !== "false"; +// MSW 启用时跳过 SSR 查询(P1-5):浏览器端由 MSW SW 拦截 /api/graphql +const MSW_ENABLED = process.env.NEXT_PUBLIC_MSW === "1"; + /** * 创建 Apollo Client 实例。 * @@ -54,7 +57,8 @@ export function createApolloClient( const isServer = options.serverSide ?? typeof window === "undefined"; const httpLink = new HttpLink({ - uri: isServer ? SERVER_APOLLO_ROUTER_URL : CLIENT_PROXY_URL, + // MSW 启用时(P1-5):SSR 端也走同域 /api/graphql,由 Route Handler 返回 mock 数据 + uri: isServer && !MSW_ENABLED ? SERVER_APOLLO_ROUTER_URL : CLIENT_PROXY_URL, // 客户端:同域请求,cookie 自动随行;服务端:直连 router 不需要 cookie credentials: isServer ? "omit" : "include", }); diff --git a/apps/portal-shell/src/mocks/browser.ts b/apps/portal-shell/src/mocks/browser.ts new file mode 100644 index 0000000..482d88d --- /dev/null +++ b/apps/portal-shell/src/mocks/browser.ts @@ -0,0 +1,13 @@ +/** + * MSW Browser Worker(P1-5,ARCHITECTURE.md §3.4 V3-A7 / §10 P1-5) + * + * 浏览器端 setupWorker,拦截 /api/graphql 与 /api/auth/* 请求。 + * 仅在 NEXT_PUBLIC_MSW=1 时由 index.ts 动态导入。 + * + * 关联:ARCHITECTURE.md §3.4 V3-A7、§10 P1-5 + */ +import { setupWorker } from "msw/browser"; + +import { handlers } from "./handlers"; + +export const worker = setupWorker(...handlers); diff --git a/apps/portal-shell/src/mocks/empty.ts b/apps/portal-shell/src/mocks/empty.ts new file mode 100644 index 0000000..4767c72 --- /dev/null +++ b/apps/portal-shell/src/mocks/empty.ts @@ -0,0 +1,32 @@ +/** + * Mocks 空 stub(P1-5,ARCHITECTURE.md §3.4 V3-A7 / §10 P1-5) + * + * 生产构建时由 next.config.js 的 resolveAlias/resolve.alias 把 + * `@/mocks`(MswProvider 的 dynamic import)和 `@/mocks/graphql-data` + * (route.ts 的 dynamic import)都重定向到本文件, + * 确保生产 bundle 完全不含真实 mock 数据。 + * + * 本文件导出与 mocks/index.ts、mocks/graphql-data.ts 相同签名的函数, + * 但都是空实现(不会被实际调用,因 MSW_ENABLED=false 时 + * MswProvider 立即返回,route.ts 不会进入 MSW 分支)。 + * + * 关联:ARCHITECTURE.md §3.4 V3-A7、§10 P1-5 + */ + +/** + * initMocks 空 stub(对应 mocks/index.ts)。 + * MswProvider 中 `if (!MSW_ENABLED) return;` 守护,MSW=false 时不会调用。 + */ +export async function initMocks(): Promise { + // no-op +} + +/** + * graphqlResponse 空 stub(对应 mocks/graphql-data.ts)。 + * route.ts 中 `if (MSW_ENABLED)` 守护,MSW=false 时不会调用。 + */ +export function graphqlResponse( + _operationName: string | undefined, +): Record { + return { data: null }; +} diff --git a/apps/portal-shell/src/mocks/graphql-data.ts b/apps/portal-shell/src/mocks/graphql-data.ts new file mode 100644 index 0000000..4c5d8d6 --- /dev/null +++ b/apps/portal-shell/src/mocks/graphql-data.ts @@ -0,0 +1,437 @@ +/** + * GraphQL Mock 数据(P1-5,ARCHITECTURE.md §3.4 V3-A7 / §10 P1-5) + * + * 被 handlers.ts(MSW browser worker)和 route.ts(/api/graphql Route Handler)共用。 + * SSR 端走 /api/graphql → Route Handler → 返回 mock 数据(不连接后端)。 + * 客户端走 /api/graphql → MSW browser worker 拦截 → 返回 mock 数据。 + * + * 关联:ARCHITECTURE.md §3.4 V3-A7、§5.1、§10 P1-5 + */ + +// ── Mock Data ────────────────────────────────────────────────── + +const mockTeacherDashboard = { + user_id: "dev-teacher-001", + total_classes: 5, + total_students: 142, + class_avg_score: 82.5, + pending_homework_count: 12, + classes: { + class_id: "cls-001", + class_name: "高三(1)班", + student_count: 38, + average_score: 85.2, + }, + top_students: [ + { + student_id: "stu-001", + student_name: "张明", + score: 98, + rank_in_class: 1, + }, + { + student_id: "stu-002", + student_name: "李华", + score: 95, + rank_in_class: 2, + }, + { + student_id: "stu-003", + student_name: "王芳", + score: 93, + rank_in_class: 3, + }, + ], + recent_warnings: { + warning_id: "warn-001", + warning_type: "score_drop", + target_id: "stu-005", + target_name: "赵六", + threshold: 60, + current_value: 45, + severity: "high", + occurred_at: "2026-07-20T10:00:00Z", + }, +}; + +const mockStudentDashboard = { + user_id: "dev-student-001", + avg_score: 88.5, + class_rank: 5, + total_students: 42, + weak_points: [ + { + knowledge_point_id: "kp-001", + title: "二次函数", + mastery: 0.45, + error_count: 8, + }, + { + knowledge_point_id: "kp-002", + title: "概率统计", + mastery: 0.62, + error_count: 5, + }, + ], + recent_trends: [ + { date: "2026-07-15", score: 85 }, + { date: "2026-07-18", score: 90 }, + { date: "2026-07-21", score: 88 }, + ], + pending_homework: 3, +}; + +const mockParentDashboard = { + user_id: "dev-parent-001", + student_id: "dev-student-001", + child_avg_score: 88.5, + child_class_rank: 5, + total_class_students: 42, + child_weak_points: [ + { + knowledge_point_id: "kp-001", + title: "二次函数", + mastery: 0.45, + error_count: 8, + }, + ], + child_warnings: { + warning_id: "warn-002", + warning_type: "score_drop", + target_id: "dev-student-001", + target_name: "张小明", + threshold: 60, + current_value: 45, + severity: "medium", + occurred_at: "2026-07-19T14:00:00Z", + }, +}; + +const mockAdminDashboard = { + user_id: "dev-admin-001", + total_teachers: 28, + total_students: 1200, + total_classes: 36, + school_avg_score: 79.8, + recent_warnings: { + warning_id: "warn-003", + warning_type: "attendance", + target_id: "cls-005", + target_name: "高二(5)班", + threshold: 0.9, + current_value: 0.72, + severity: "high", + occurred_at: "2026-07-22T08:00:00Z", + }, + ai_usage: { + total_requests: 1520, + total_tokens: 480000, + total_cost_cents: 9600, + by_provider: [ + { + provider: "doubao", + request_count: 800, + total_tokens: 250000, + cost_cents: 5000, + }, + { + provider: "deepseek", + request_count: 720, + total_tokens: 230000, + cost_cents: 4600, + }, + ], + }, +}; + +const mockUsers = { + items: [ + { + id: "usr-001", + name: "张老师", + email: "zhang@edu.cn", + role: "teacher", + status: "active", + createdAt: "2026-06-01T00:00:00Z", + }, + { + id: "usr-002", + name: "李老师", + email: "li@edu.cn", + role: "teacher", + status: "active", + createdAt: "2026-06-15T00:00:00Z", + }, + { + id: "usr-003", + name: "管理员", + email: "admin@edu.cn", + role: "admin", + status: "active", + createdAt: "2026-05-01T00:00:00Z", + }, + { + id: "usr-004", + name: "王同学", + email: "wang@edu.cn", + role: "student", + status: "active", + createdAt: "2026-07-01T00:00:00Z", + }, + { + id: "usr-005", + name: "赵家长", + email: "zhao@edu.cn", + role: "parent", + status: "suspended", + createdAt: "2026-07-10T00:00:00Z", + }, + ], + total: 5, +}; + +const mockQuestions = [ + { + id: "q-001", + type: "single_choice", + difficulty: "easy", + content: "下列哪个是质数?", + options: ["4", "7", "9", "15"], + answer: "B", + tags: ["数论", "质数"], + }, + { + id: "q-002", + type: "multiple_choice", + difficulty: "medium", + content: "下列哪些是偶数?", + options: ["2", "3", "4", "5"], + answer: "AC", + tags: ["数论", "偶数"], + }, + { + id: "q-003", + type: "fill_blank", + difficulty: "hard", + content: "sin(30°) = ?", + options: null, + answer: "0.5", + tags: ["三角函数"], + }, +]; + +const mockTextbooks = [ + { + id: "tb-001", + title: "高中数学必修一", + author: "人民教育出版社", + publisher: "人教版", + isbn: "978-7-107-000001", + chapters: [ + { id: "ch-001", title: "第一章 集合与函数" }, + { id: "ch-002", title: "第二章 基本初等函数" }, + ], + }, + { + id: "tb-002", + title: "高中物理必修一", + author: "人民教育出版社", + publisher: "人教版", + isbn: "978-7-107-000002", + chapters: [ + { id: "ch-003", title: "第一章 运动的描述" }, + { id: "ch-004", title: "第二章 匀变速直线运动" }, + ], + }, +]; + +const mockGrades = [ + { + student_id: "stu-001", + student_name: "张明", + exam_id: "exam-001", + exam_name: "期中考试", + score: 95, + rank: 1, + }, + { + student_id: "stu-002", + student_name: "李华", + exam_id: "exam-001", + exam_name: "期中考试", + score: 88, + rank: 2, + }, + { + student_id: "stu-003", + student_name: "王芳", + exam_id: "exam-001", + exam_name: "期中考试", + score: 76, + rank: 3, + }, +]; + +// ── GraphQL Response ─────────────────────────────────────────── + +/** + * 根据 operationName 返回 mock GraphQL 响应。 + */ +export function graphqlResponse( + operationName: string | undefined, +): Record { + switch (operationName) { + // ── Dashboard 域 ── + case "GetTeacherDashboard": + return { data: { teacherDashboard: mockTeacherDashboard } }; + case "GetStudentDashboard": + return { data: { studentDashboard: mockStudentDashboard } }; + case "GetParentDashboard": + return { data: { parentDashboard: mockParentDashboard } }; + case "GetAdminDashboard": + return { data: { adminDashboard: mockAdminDashboard } }; + case "GetWarnings": + return { + data: { + warnings: { + warnings: [mockTeacherDashboard.recent_warnings], + total: 1, + }, + }, + }; + case "GetErrorBookStats": + return { + data: { + errorBookStats: { + student_id: "dev-student-001", + total_error_questions: 15, + total_error_count: 42, + by_knowledge_point: [ + { + knowledge_point_id: "kp-001", + title: "二次函数", + error_count: 12, + question_count: 20, + error_rate: 0.6, + }, + ], + recent_7d_errors: 5, + }, + }, + }; + + // ── Users 域 ── + case "GetUsers": + return { data: { users: mockUsers } }; + case "UpdateUserStatus": + return { + data: { updateUserStatus: { id: "usr-005", status: "active" } }, + }; + case "UpdateUserRole": + return { data: { updateUserRole: { id: "usr-001", role: "admin" } } }; + case "GetRoles": + return { + data: { + roles: [ + { + id: "role-001", + name: "admin", + permissions: [ + { + id: "perm-001", + name: "user.read", + resource: "user", + action: "read", + }, + { + id: "perm-002", + name: "user.write", + resource: "user", + action: "write", + }, + ], + }, + { + id: "role-002", + name: "teacher", + permissions: [ + { + id: "perm-003", + name: "class.read", + resource: "class", + action: "read", + }, + ], + }, + ], + }, + }; + case "GetPermissions": + return { + data: { + permissions: [ + { + id: "perm-001", + name: "user.read", + resource: "user", + action: "read", + description: "查看用户", + }, + { + id: "perm-002", + name: "user.write", + resource: "user", + action: "write", + description: "编辑用户", + }, + ], + }, + }; + + // ── Exams 域 ── + case "GetQuestions": + return { data: { questions: mockQuestions } }; + case "GetTextbooks": + return { data: { textbooks: mockTextbooks } }; + case "GetLessonPlans": + return { + data: { + lessonPlans: [ + { + id: "lp-001", + title: "集合的概念", + objectives: "理解集合的定义与表示方法", + content: "集合是数学中最基本的概念之一...", + resources: ["教材P1-10", "练习册P1-5"], + }, + ], + }, + }; + + // ── Grades 域(预留) ── + case "GetGrades": + return { data: { grades: mockGrades } }; + + // ── 通用 ── + case "GetNotificationsList": + return { + data: { + notifications: { + items: [ + { + id: "notif-001", + title: "欢迎使用 Edu Portal", + body: "系统已就绪,开始您的教学之旅吧!", + createdAt: "2026-07-22T08:00:00Z", + type: "system", + }, + ], + total: 1, + }, + }, + }; + + default: + return { data: null }; + } +} diff --git a/apps/portal-shell/src/mocks/handlers.ts b/apps/portal-shell/src/mocks/handlers.ts new file mode 100644 index 0000000..ef692b9 --- /dev/null +++ b/apps/portal-shell/src/mocks/handlers.ts @@ -0,0 +1,63 @@ +/** + * MSW Handlers(P1-5,ARCHITECTURE.md §3.4 V3-A7 / §10 P1-5) + * + * 覆盖四域起步:dashboard / users / exams / grades + * 拦截 POST /api/graphql(Apollo Client 同域代理路径) + * 按 operationName 路由,未命中返回 HttpResponse.json({ data: null }) + * + * Mock 数据与 graphqlResponse 函数提取到 graphql-data.ts, + * 供 handlers.ts(MSW browser)和 route.ts(/api/graphql Route Handler)共用, + * 确保 SSR 端和客户端返回一致的 mock 数据。 + * + * 启用条件:NEXT_PUBLIC_MSW=1(生产构建永不包含) + * 关联:ARCHITECTURE.md §3.4 V3-A7、§5.1、§10 P1-5 + */ +import { http, HttpResponse } from "msw"; +import { graphqlResponse } from "./graphql-data"; + +// ── Handlers 导出 ────────────────────────────────────────────── + +// Apollo Router URL(SSR 端直连兜底,MSW server 拦截用) +const APOLLO_ROUTER_GRAPHQL = + process.env.APOLLO_ROUTER_URL || "http://localhost:3000/graphql"; + +export const handlers = [ + // GraphQL 同域代理(客户端 Apollo Client) + http.post("/api/graphql", async ({ request }) => { + const body = (await request.json()) as { operationName?: string }; + return HttpResponse.json(graphqlResponse(body.operationName)); + }), + + // GraphQL SSR 直连兜底(msw/node server 拦截 RSC 端 fetch) + http.post(APOLLO_ROUTER_GRAPHQL, async ({ request }) => { + const body = (await request.json()) as { operationName?: string }; + return HttpResponse.json(graphqlResponse(body.operationName)); + }), + + // 登录兜底(DEV_MODE 使用) + http.post("/api/auth/login", async ({ request }) => { + const body = (await request.json()) as { + email?: string; + password?: string; + }; + return HttpResponse.json({ + success: true, + user: { + id: "dev-user-001", + email: body.email ?? "dev@edu.local", + name: "开发用户", + role: "teacher", + permissions: ["*"], + dataScope: "all", + }, + }); + }), + + // 健康检查兜底 + http.get("/api/health", () => + HttpResponse.json({ status: "ok", timestamp: new Date().toISOString() }), + ), + http.get("/api/ready", () => + HttpResponse.json({ status: "ready", timestamp: new Date().toISOString() }), + ), +]; diff --git a/apps/portal-shell/src/mocks/index.ts b/apps/portal-shell/src/mocks/index.ts new file mode 100644 index 0000000..ff4de0d --- /dev/null +++ b/apps/portal-shell/src/mocks/index.ts @@ -0,0 +1,30 @@ +/** + * MSW 启用入口(P1-5,ARCHITECTURE.md §3.4 V3-A7 / §10 P1-5) + * + * NEXT_PUBLIC_MSW=1 时启用: + * - 浏览器端:动态导入 ./browser,启动 Service Worker + * - 服务端:动态导入 ./server,启动 Node.js 拦截 + * + * 生产构建安全: + * - NEXT_PUBLIC_MSW 不设或非 "1" 时,此函数立即返回(无副作用) + * - webpack/turbopack 会将 mocks/* 代码分割到独立 chunk + * - 生产环境 NEXT_PUBLIC_MSW 编译时为 undefined,dynamic import 被 tree-shake + * + * 关联:ARCHITECTURE.md §3.4 V3-A7、§5.1、§10 P1-5 + */ +const MSW_ENABLED = process.env.NEXT_PUBLIC_MSW === "1"; + +export async function initMocks(): Promise { + if (!MSW_ENABLED) return; + + if (typeof window === "undefined") { + const { server } = await import("./server"); + server.listen({ onUnhandledRequest: "bypass" }); + } else { + const { worker } = await import("./browser"); + await worker.start({ + onUnhandledRequest: "bypass", + serviceWorker: { url: "/mockServiceWorker.js" }, + }); + } +} diff --git a/apps/portal-shell/src/mocks/server.ts b/apps/portal-shell/src/mocks/server.ts new file mode 100644 index 0000000..c8bb334 --- /dev/null +++ b/apps/portal-shell/src/mocks/server.ts @@ -0,0 +1,13 @@ +/** + * MSW Server(P1-5,ARCHITECTURE.md §3.4 V3-A7 / §10 P1-5) + * + * Node.js 端 setupServer,用于 SSR + 测试环境。 + * 仅在 NEXT_PUBLIC_MSW=1 时由 index.ts 动态导入。 + * + * 关联:ARCHITECTURE.md §3.4 V3-A7、§10 P1-5 + */ +import { setupServer } from "msw/node"; + +import { handlers } from "./handlers"; + +export const server = setupServer(...handlers); diff --git a/apps/portal-shell/src/providers/MswProvider.tsx b/apps/portal-shell/src/providers/MswProvider.tsx new file mode 100644 index 0000000..d8a5f84 --- /dev/null +++ b/apps/portal-shell/src/providers/MswProvider.tsx @@ -0,0 +1,42 @@ +"use client"; + +/** + * MswProvider(P1-5,ARCHITECTURE.md §3.4 V3-A7 / §10 P1-5) + * + * NEXT_PUBLIC_MSW=1 时在客户端启动 MSW Service Worker。 + * - MSW 启用时阻塞子组件渲染直到 worker ready(避免 mock 未就绪时请求穿透) + * - MSW 未启用时直接渲染子组件(零开销,生产构建安全) + * + * 生产构建安全(关键): + * - 静态 `import { initMocks } from "@/mocks"` 配合 next.config.js resolveAlias + * - MSW 关闭时 `@/mocks` 被重定向到 `src/mocks/empty.ts`(no-op 实现) + * - Next.js 在客户端 bundle 内联 NEXT_PUBLIC_MSW 为字符串字面量, + * `MSW_ENABLED === "1"` 编译期求值为 false,整个 useEffect 分支被 dead-code 消除 + * + * 关联:ARCHITECTURE.md §3.4 V3-A7、§5.1、§10 P1-5 + */ +import { useEffect, useState, type ReactNode } from "react"; + +import { initMocks } from "@/mocks"; + +const MSW_ENABLED = process.env.NEXT_PUBLIC_MSW === "1"; + +export function MswProvider({ children }: { children: ReactNode }): ReactNode { + const [mswReady, setMswReady] = useState(!MSW_ENABLED); + + useEffect(() => { + if (!MSW_ENABLED) return; + let mounted = true; + initMocks().finally(() => { + if (mounted) setMswReady(true); + }); + return () => { + mounted = false; + }; + }, []); + + // MSW 启用时阻塞渲染,避免 mock 未就绪时请求穿透到后端 + if (!mswReady) return null; + + return <>{children}; +} diff --git a/apps/portal-shell/src/shell/ClientShell.tsx b/apps/portal-shell/src/shell/ClientShell.tsx index 3a0a73c..3f571e3 100644 --- a/apps/portal-shell/src/shell/ClientShell.tsx +++ b/apps/portal-shell/src/shell/ClientShell.tsx @@ -17,6 +17,7 @@ import { use, useState, type ReactNode } from "react"; import { ApolloProvider } from "@/providers/ApolloProvider"; import { AuthProvider, type AuthUser } from "@/providers/AuthProvider"; +import { MswProvider } from "@/providers/MswProvider"; import { ThemeProvider } from "@/providers/ThemeProvider"; import { Shell } from "./Shell"; import { usePluginConfig } from "@/lib/usePluginConfig"; @@ -103,30 +104,32 @@ export function ClientShell({ const useStreaming = Boolean(configPromise); return ( - - - - {useStreaming && configPromise ? ( - - ) : ( - setConfigChanged(true)} - configChanged={configChanged} - onDismiss={() => setConfigChanged(false)} - /> - )} - - - + + + + + {useStreaming && configPromise ? ( + + ) : ( + setConfigChanged(true)} + configChanged={configChanged} + onDismiss={() => setConfigChanged(false)} + /> + )} + + + + ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10d664a..a62163b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,7 +186,7 @@ importers: version: 5.101.2(react@18.3.0) '@urql/next': specifier: ^1.1.0 - version: 1.1.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0)) + version: 1.1.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0)) clsx: specifier: ^2.1.0 version: 2.1.1 @@ -369,6 +369,9 @@ importers: lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.7) + msw: + specifier: 2.7.0 + version: 2.7.0(@types/node@22.20.1)(typescript@5.6.2) next: specifier: ^16.0.10 version: 16.2.10(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -456,7 +459,7 @@ importers: version: 5.6.2 vitest: specifier: ^2.0.0 - version: 2.1.0(@types/node@22.20.1)(jsdom@25.0.0)(lightningcss@1.32.0)(msw@2.15.0(@types/node@22.20.1)(typescript@5.6.2))(terser@5.49.0) + version: 2.1.0(@types/node@22.20.1)(jsdom@25.0.0)(lightningcss@1.32.0)(msw@2.7.0(@types/node@22.20.1)(typescript@5.6.2))(terser@5.49.0) apps/student-portal: dependencies: @@ -2051,6 +2054,15 @@ packages: '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -3274,10 +3286,23 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + '@inquirer/ansi@2.0.7': resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@6.1.1': resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -3287,6 +3312,15 @@ packages: '@types/node': optional: true + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@11.2.1': resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -3296,10 +3330,23 @@ packages: '@types/node': optional: true + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + '@inquirer/figures@2.0.7': resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@4.0.7': resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -3352,15 +3399,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.10': resolution: {integrity: sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==} - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -3489,6 +3530,10 @@ packages: '@module-federation/webpack-bundler-runtime@2.7.0': resolution: {integrity: sha512-3qLRIqcZVBNgJrZpiEzcTP8a6+mCdUV1QGk/XljawsQHyNieMVdLQtdLvkoF5Z5kRXNMovq+wv3vchKOMWyA8w==} + '@mswjs/interceptors@0.37.6': + resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} + engines: {node: '>=18'} + '@mswjs/interceptors@0.41.9': resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} @@ -6374,6 +6419,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -6544,6 +6592,9 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -7512,8 +7563,8 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} cookie@1.1.1: @@ -8664,6 +8715,9 @@ packages: header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} @@ -9695,6 +9749,16 @@ packages: typescript: optional: true + msw@2.7.0: + resolution: {integrity: sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + multer@2.0.2: resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} engines: {node: '>= 10.16.0'} @@ -9706,6 +9770,10 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -11459,14 +11527,14 @@ packages: resolution: {integrity: sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==} engines: {node: '>=12.20'} - type-fest@2.5.0: - resolution: {integrity: sha512-wB5vE+XXZ2g2mDRo18yZMae1joUhquomLTm+BkxeuRHnwmrNWzVPNrFah9z7pjsKNiVAaJL33+uQbgbPSARyqw==} - engines: {node: '>=12.20'} - type-fest@3.9.0: resolution: {integrity: sha512-hR8JP2e8UiH7SME5JZjsobBlEiatFoxpzCP+R3ZeCo7kAaG1jXQE5X/buLzogM6GJu8le9Y4OcfNuIQX0rZskA==} engines: {node: '>=14.16'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-fest@5.8.0: resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} engines: {node: '>=20'} @@ -12067,6 +12135,10 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + zen-observable-ts@1.2.5: resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==} @@ -12489,6 +12561,19 @@ snapshots: '@borewit/text-codec@0.2.2': {} + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.2 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@colors/colors@1.5.0': optional: true @@ -13849,8 +13934,17 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.7': {} + '@inquirer/confirm@5.1.21(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.1) + '@inquirer/type': 3.0.10(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + '@inquirer/confirm@6.1.1(@types/node@22.20.1)': dependencies: '@inquirer/core': 11.2.1(@types/node@22.20.1) @@ -13858,6 +13952,19 @@ snapshots: optionalDependencies: '@types/node': 22.20.1 + '@inquirer/core@10.3.2(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.1) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.1 + '@inquirer/core@11.2.1(@types/node@22.20.1)': dependencies: '@inquirer/ansi': 2.0.7 @@ -13870,8 +13977,14 @@ snapshots: optionalDependencies: '@types/node': 22.20.1 + '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.7': {} + '@inquirer/type@3.0.10(@types/node@22.20.1)': + optionalDependencies: + '@types/node': 22.20.1 + '@inquirer/type@4.0.7(@types/node@22.20.1)': optionalDependencies: '@types/node': 22.20.1 @@ -13899,7 +14012,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/gen-mapping@0.3.2': @@ -13930,18 +14043,14 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.10': {} - '@jridgewell/sourcemap-codec@1.4.14': {} - '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.5.5 '@js-sdsl/ordered-map@4.4.2': {} @@ -14061,7 +14170,7 @@ snapshots: next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0) react: 18.3.0 react-dom: 18.3.0(react@18.3.0) - styled-jsx: 5.1.6(react@18.3.0) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@18.3.0) webpack: 5.92.1(lightningcss@1.32.0)(postcss@8.5.18) transitivePeerDependencies: - '@rspack/core' @@ -14132,6 +14241,15 @@ snapshots: '@module-federation/runtime': 2.7.0 '@module-federation/sdk': 2.7.0 + '@mswjs/interceptors@0.37.6': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@mswjs/interceptors@0.41.9': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -17544,6 +17662,8 @@ snapshots: dependencies: '@types/node': 22.20.1 + '@types/cookie@0.6.0': {} + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -17741,6 +17861,8 @@ snapshots: dependencies: '@types/node': 22.20.1 + '@types/tough-cookie@4.0.5': {} + '@types/uuid@10.0.0': {} '@types/uuid@9.0.8': {} @@ -17859,7 +17981,7 @@ snapshots: transitivePeerDependencies: - graphql - '@urql/next@1.1.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))': + '@urql/next@1.1.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))': dependencies: next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0) react: 18.3.0 @@ -17910,6 +18032,15 @@ snapshots: msw: 2.15.0(@types/node@22.20.1)(typescript@5.6.2) vite: 5.0.0(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.49.0) + '@vitest/mocker@2.1.0(@vitest/spy@2.1.0)(msw@2.7.0(@types/node@22.20.1)(typescript@5.6.2))(vite@5.0.0(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.49.0))': + dependencies: + '@vitest/spy': 2.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.7.0(@types/node@22.20.1)(typescript@5.6.2) + vite: 5.0.0(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.49.0) + '@vitest/pretty-format@2.1.0': dependencies: tinyrainbow: 1.2.0 @@ -18859,7 +18990,7 @@ snapshots: cookie-signature@1.0.7: {} - cookie@0.7.1: {} + cookie@0.7.2: {} cookie@1.1.1: {} @@ -19629,7 +19760,7 @@ snapshots: body-parser: 1.20.6 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.7.1 + cookie: 0.7.2 cookie-signature: 1.0.7 debug: 2.6.9 depd: 2.0.0 @@ -20199,6 +20330,8 @@ snapshots: capital-case: 1.0.4 tslib: 2.8.1 + headers-polyfill@4.0.3: {} + headers-polyfill@5.0.1: dependencies: '@types/set-cookie-parser': 2.4.10 @@ -21221,6 +21354,31 @@ snapshots: transitivePeerDependencies: - '@types/node' + msw@2.7.0(@types/node@22.20.1)(typescript@5.6.2): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.1.21(@types/node@22.20.1) + '@mswjs/interceptors': 0.37.6 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.6 + graphql: 16.14.2 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + strict-event-emitter: 0.5.1 + type-fest: 4.41.0 + yargs: 17.7.3 + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - '@types/node' + multer@2.0.2: dependencies: append-field: 1.0.0 @@ -21235,6 +21393,8 @@ snapshots: mute-stream@1.0.0: {} + mute-stream@2.0.0: {} + mute-stream@3.0.0: {} mysql2@3.22.6(@types/node@22.20.1): @@ -22025,14 +22185,14 @@ snapshots: dependencies: find-up: 6.3.0 read-pkg: 7.1.0 - type-fest: 2.5.0 + type-fest: 2.13.0 read-pkg@7.1.0: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 3.0.2 parse-json: 5.2.0 - type-fest: 2.5.0 + type-fest: 2.13.0 readable-stream@2.3.8: dependencies: @@ -22772,6 +22932,14 @@ snapshots: optionalDependencies: '@babel/core': 7.29.7 + styled-jsx@5.1.6(@babel/core@7.29.7)(react@18.3.0): + dependencies: + client-only: 0.0.1 + react: 18.3.0 + optionalDependencies: + '@babel/core': 7.29.7 + optional: true + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7): dependencies: client-only: 0.0.1 @@ -22779,12 +22947,6 @@ snapshots: optionalDependencies: '@babel/core': 7.29.7 - styled-jsx@5.1.6(react@18.3.0): - dependencies: - client-only: 0.0.1 - react: 18.3.0 - optional: true - subscriptions-transport-ws@0.11.0(graphql@16.14.2): dependencies: backo2: 1.0.2 @@ -23194,10 +23356,10 @@ snapshots: type-fest@2.13.0: {} - type-fest@2.5.0: {} - type-fest@3.9.0: {} + type-fest@4.41.0: {} + type-fest@5.8.0: dependencies: tagged-tag: 1.0.0 @@ -23511,6 +23673,40 @@ snapshots: - supports-color - terser + vitest@2.1.0(@types/node@22.20.1)(jsdom@25.0.0)(lightningcss@1.32.0)(msw@2.7.0(@types/node@22.20.1)(typescript@5.6.2))(terser@5.49.0): + dependencies: + '@vitest/expect': 2.1.0 + '@vitest/mocker': 2.1.0(@vitest/spy@2.1.0)(msw@2.7.0(@types/node@22.20.1)(typescript@5.6.2))(vite@5.0.0(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.49.0)) + '@vitest/pretty-format': 2.1.0 + '@vitest/runner': 2.1.0 + '@vitest/snapshot': 2.1.0 + '@vitest/spy': 2.1.0 + '@vitest/utils': 2.1.0 + chai: 5.1.1 + debug: 4.4.3 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinyexec: 0.3.0 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 + vite: 5.0.0(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.49.0) + vite-node: 2.1.0(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.49.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + jsdom: 25.0.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - stylus + - sugarss + - supports-color + - terser + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -23763,7 +23959,7 @@ snapshots: wide-align@1.1.5: dependencies: - string-width: 4.1.0 + string-width: 4.2.3 wonka@4.0.14: {} @@ -23863,6 +24059,8 @@ snapshots: yocto-queue@1.2.2: {} + yoctocolors-cjs@2.1.3: {} + zen-observable-ts@1.2.5: dependencies: zen-observable: 0.8.15