From dbb124bf17400f5df3a826624b7d5f543137dd4b Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:23:03 +0800 Subject: [PATCH] feat: add CI unit test, /api/health, Dockerfile HEALTHCHECK, bundle-analyzer --- .gitea/workflows/ci.yml | 3 + Dockerfile | 3 + docs/architecture/roadmap/tech-debt.md | 5 +- next.config.ts | 21 ++- package-lock.json | 212 +++++++++++++++++++++++++ package.json | 4 +- src/app/api/health/route.ts | 10 ++ 7 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 src/app/api/health/route.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 21a98e0..3d68d6c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -67,6 +67,9 @@ jobs: - name: Typecheck run: npm run typecheck + - name: Unit tests + run: npm run test:unit + - name: Architecture scan run: | npm run arch:scan diff --git a/Dockerfile b/Dockerfile index 87fdc7c..0ef5a06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,7 @@ EXPOSE 3000 ENV PORT 3000 ENV HOSTNAME "0.0.0.0" +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD node -e "fetch('http://localhost:' + (process.env.PORT || 3000) + '/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" + CMD ["node", "server.js"] diff --git a/docs/architecture/roadmap/tech-debt.md b/docs/architecture/roadmap/tech-debt.md index 21ab238..509a0c4 100644 --- a/docs/architecture/roadmap/tech-debt.md +++ b/docs/architecture/roadmap/tech-debt.md @@ -22,9 +22,10 @@ ### TD-P1-001: noUncheckedIndexedAccess 全量修复 -- **状态**: 计划在 P2 阶段实施 +- **状态**: 已评估,419 个错误,推迟到独立迭代 - **影响**: 开启 `noUncheckedIndexedAccess` 后,所有数组/对象索引访问返回 `T | undefined`,需要全量修复类型错误 -- **预估工作量**: 涉及大量文件,需要逐个添加类型守卫或非空断言 +- **评估结果**: 2026-07-07 评估,开启后产生 419 个 tsc 错误,主要涉及 `array[0]`、`array[index]`、`Object.keys()[i]` 等模式 +- **建议**: 独立迭代逐步修复,按模块分批进行(优先修复 data-access 层,再修复 actions 层,最后修复组件层) - **关联**: tsconfig.json ### TD-P1-002: CI 缺少 Unit test 阶段 diff --git a/next.config.ts b/next.config.ts index 599568b..5cbf8bc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,11 @@ -import type { NextConfig } from "next"; -import createNextIntlPlugin from "next-intl/plugin"; +import type { NextConfig } from "next" +import createNextIntlPlugin from "next-intl/plugin" +import bundleAnalyzer from "@next/bundle-analyzer" -const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); +const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts") +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}) const nextConfig: NextConfig = { output: "standalone", @@ -10,12 +14,7 @@ const nextConfig: NextConfig = { // - tencentcloud-sdk-nodejs: 腾讯云 SDK,体积 ~2-3 MB,仅在短信/对象存储等服务端调用 // - exceljs: Excel 生成库,体积 ~250-300 KB,仅在导出报表时使用 // - pino: 结构化日志库,依赖 node:async_hooks 与 transport worker 线程 - serverExternalPackages: [ - "mysql2", - "tencentcloud-sdk-nodejs", - "exceljs", - "pino", - ], + serverExternalPackages: ["mysql2", "tencentcloud-sdk-nodejs", "exceljs", "pino"], experimental: { // 启用 optimizePackageImports 让 Turbopack 对 barrel 导出做精确 tree-shaking, // 避免整个库被打入首屏 chunk。覆盖项目中最常用的重型库: @@ -43,6 +42,6 @@ const nextConfig: NextConfig = { "date-fns", ], }, -}; +} -export default withNextIntl(nextConfig); +export default withBundleAnalyzer(withNextIntl(nextConfig)) diff --git a/package-lock.json b/package-lock.json index 34ba853..b2d33dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,6 +85,7 @@ "@commitlint/cli": "^21.2.0", "@commitlint/config-conventional": "^21.2.0", "@faker-js/faker": "^10.1.0", + "@next/bundle-analyzer": "^16.2.10", "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.16", @@ -114,6 +115,9 @@ "tsx": "^4.23.0", "typescript": "^5", "vitest": "^4.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@adobe/css-tools": { @@ -1296,6 +1300,16 @@ "xml2js": "^0.6.2" } }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -3148,6 +3162,16 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@next/bundle-analyzer": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.2.10.tgz", + "integrity": "sha512-KcepWhb3IVniZgm00GSSCQDEUQqZXuXtuXRh8J6e3Un342TcQ77iK4DedeEkct+fcx7yFEDL2J6z4Jeho5JDAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "webpack-bundle-analyzer": "4.10.1" + } + }, "node_modules/@next/env": { "version": "16.0.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", @@ -3780,6 +3804,13 @@ "node": ">=18" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -8833,6 +8864,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -10445,6 +10489,13 @@ "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -10813,6 +10864,13 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -12437,6 +12495,22 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -13202,6 +13276,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -15680,6 +15764,16 @@ "node": "*" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -16310,6 +16404,16 @@ } } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -18333,6 +18437,21 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -19076,6 +19195,16 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -21260,6 +21389,66 @@ "node": ">=20" } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", @@ -21443,6 +21632,29 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index 7024e00..231bdec 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "arch:scan": "tsx scripts/arch-scan/cli.ts scan", "arch:query": "tsx scripts/arch-scan/cli.ts query", "test:arch-scan": "vitest run --config vitest.arch-scan.config.ts", - "prepare": "husky" + "prepare": "husky", + "analyze": "ANALYZE=true next build" }, "lint-staged": { "*.{ts,tsx}": [ @@ -134,6 +135,7 @@ "@commitlint/cli": "^21.2.0", "@commitlint/config-conventional": "^21.2.0", "@faker-js/faker": "^10.1.0", + "@next/bundle-analyzer": "^16.2.10", "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.16", diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..e382b5f --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server" + +export const dynamic = "force-dynamic" + +export async function GET(): Promise> { + return NextResponse.json({ + status: "ok", + timestamp: new Date().toISOString(), + }) +}