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.
This commit is contained in:
@@ -757,7 +757,7 @@ export default async function ExamsPage(): Promise<React.ReactElement> {
|
||||
- **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<React.ReactElement> {
|
||||
| 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<React.ReactElement> {
|
||||
- `GET /login`(`Cookie: NEXT_LOCALE=en`)→ `<html lang="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。
|
||||
|
||||
2
apps/portal-shell/next-env.d.ts
vendored
2
apps/portal-shell/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
307
apps/portal-shell/public/mockServiceWorker.js
Normal file
307
apps/portal-shell/public/mockServiceWorker.js
Normal file
@@ -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
|
||||
}
|
||||
@@ -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<NextResponse> {
|
||||
// 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<NextResponse> {
|
||||
ok: true,
|
||||
proxy: "/api/graphql",
|
||||
upstream: UPSTREAM_URL,
|
||||
msw: MSW_ENABLED,
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
13
apps/portal-shell/src/mocks/browser.ts
Normal file
13
apps/portal-shell/src/mocks/browser.ts
Normal file
@@ -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);
|
||||
32
apps/portal-shell/src/mocks/empty.ts
Normal file
32
apps/portal-shell/src/mocks/empty.ts
Normal file
@@ -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<void> {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* graphqlResponse 空 stub(对应 mocks/graphql-data.ts)。
|
||||
* route.ts 中 `if (MSW_ENABLED)` 守护,MSW=false 时不会调用。
|
||||
*/
|
||||
export function graphqlResponse(
|
||||
_operationName: string | undefined,
|
||||
): Record<string, unknown> {
|
||||
return { data: null };
|
||||
}
|
||||
437
apps/portal-shell/src/mocks/graphql-data.ts
Normal file
437
apps/portal-shell/src/mocks/graphql-data.ts
Normal file
@@ -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<string, unknown> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
63
apps/portal-shell/src/mocks/handlers.ts
Normal file
63
apps/portal-shell/src/mocks/handlers.ts
Normal file
@@ -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() }),
|
||||
),
|
||||
];
|
||||
30
apps/portal-shell/src/mocks/index.ts
Normal file
30
apps/portal-shell/src/mocks/index.ts
Normal file
@@ -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<void> {
|
||||
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" },
|
||||
});
|
||||
}
|
||||
}
|
||||
13
apps/portal-shell/src/mocks/server.ts
Normal file
13
apps/portal-shell/src/mocks/server.ts
Normal file
@@ -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);
|
||||
42
apps/portal-shell/src/providers/MswProvider.tsx
Normal file
42
apps/portal-shell/src/providers/MswProvider.tsx
Normal file
@@ -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}</>;
|
||||
}
|
||||
@@ -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,6 +104,7 @@ export function ClientShell({
|
||||
const useStreaming = Boolean(configPromise);
|
||||
|
||||
return (
|
||||
<MswProvider>
|
||||
<ApolloProvider>
|
||||
<AuthProvider user={user}>
|
||||
<ThemeProvider>
|
||||
@@ -127,6 +129,7 @@ export function ClientShell({
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</ApolloProvider>
|
||||
</MswProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
268
pnpm-lock.yaml
generated
268
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user