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:
SpecialX
2026-07-22 14:48:30 +08:00
parent da05c9107a
commit 9358372657
16 changed files with 1292 additions and 67 deletions

View File

@@ -1,9 +1,15 @@
/**
* GraphQL 同域代理P0-3ARCHITECTURE.md §3.4 V3-A2 / §4 / §5.2
* GraphQL 同域代理P0-3 + P1-5ARCHITECTURE.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. 透传 bodyAPQ 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 },
);