框架切换:Next.js 16 → Vite 5 + React 19 + React Router 7 状态管理:7 手写 hook → 4 Zustand slice 数据获取:直接 fetch → TanStack Query 5 (15 hooks) 表单:3-step 状态机 → RHF 7 + Zod 测试:0 → 25 单测 + 20 e2e (4 浏览器) Bundle:192 KB gzip (目标 < 300 KB) Lint:49 warning → 0 warning Type:33 any → 0 any S2-T-01 切 Vite 5 + React 19 框架 S2-T-02 Zustand 4 slice 替代 7 手写 hook S2-T-03 TanStack Query 5 + 15 hooks S2-T-04 Vitest + RTL + MSW + Chromatic 配置 S2-T-05 OpenAPI Codegen + Zod schema 复用 S2-T-06 RHF 7 + Zod 重写 FormCard S2-T-07 Playwright 视觉基线 4 浏览器 5 spec S2-T-08 Vite build + bundle < 300KB gzip G1 闸门 (全部通过): - typecheck: 0 error - lint: 0 error 0 warning - test (Vitest): 25/25 passed - test:e2e (Playwright): 20/20 passed - build (Vite): 326 modules 4.14s - codegen:check: OpenAPI types 非占位 Sprint 2 收口报告:SPRINT_2_CLOSEOUT_2026-07-03.md
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
/**
|
||
* V10 选项 B · OpenAPI Codegen 校验脚本
|
||
*
|
||
* G1 contract gate:
|
||
* - 生成文件必须存在
|
||
* - 不允许旧占位 stub 文件假通过
|
||
* - TypeScript 声明必须包含 OpenAPI paths
|
||
* - Zod client 输出必须包含可导出 contract/schema 内容
|
||
*
|
||
* 注意: 后端 OpenAPI 文档里可能合法出现“占位密钥”等业务说明,
|
||
* 不能把任意“占位”字样误判为生成文件仍是 stub。
|
||
*/
|
||
import { existsSync, readFileSync } from 'node:fs';
|
||
|
||
const TYPES_FILE = 'src/types/api-generated.d.ts';
|
||
const SCHEMAS_FILE = 'src/schemas/api-generated.ts';
|
||
|
||
function fail(message: string): never {
|
||
console.error(`❌ ${message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
function readRequired(path: string): string {
|
||
if (!existsSync(path)) {
|
||
fail(`Codegen 文件缺失: ${path}; 请运行 pnpm codegen`);
|
||
}
|
||
return readFileSync(path, 'utf-8');
|
||
}
|
||
|
||
function looksLikeStub(content: string): boolean {
|
||
const trimmed = content.trim();
|
||
const firstLines = trimmed.split(/\r?\n/).slice(0, 12).join('\n');
|
||
if (trimmed === 'export {};') return true;
|
||
if (/^\/\/\s*(占位类型|占位 schema|placeholder|stub)/i.test(firstLines)) return true;
|
||
if (trimmed.length < 500 && /占位|placeholder|stub|真实后端启动后|后端 OpenAPI 联通后/i.test(trimmed)) return true;
|
||
return false;
|
||
}
|
||
|
||
const typesContent = readRequired(TYPES_FILE);
|
||
const schemasContent = readRequired(SCHEMAS_FILE);
|
||
|
||
if (looksLikeStub(typesContent)) {
|
||
fail(`${TYPES_FILE} 仍是占位/stub 输出`);
|
||
}
|
||
|
||
if (looksLikeStub(schemasContent)) {
|
||
fail(`${SCHEMAS_FILE} 仍是占位/stub 输出`);
|
||
}
|
||
|
||
if (!/export interface paths\b/.test(typesContent)) {
|
||
fail(`${TYPES_FILE} 未包含 openapi-typescript paths 接口`);
|
||
}
|
||
|
||
if (!/\/api\//.test(typesContent)) {
|
||
fail(`${TYPES_FILE} 未包含任何 /api/ 路径`);
|
||
}
|
||
|
||
if (!/export const|export default|makeApi|z\.object/.test(schemasContent)) {
|
||
fail(`${SCHEMAS_FILE} 未包含可用的 schema/client 导出`);
|
||
}
|
||
|
||
if (/: any\b/.test(typesContent)) {
|
||
fail(`${TYPES_FILE} 包含 ': any',G1 0-any 闸门失败`);
|
||
}
|
||
|
||
console.log('✅ Codegen contract gate passed: generated OpenAPI types/schemas are non-stub');
|