build(ci): 建立单仓工具链与质量门禁
统一 pnpm 10 + Node 24.15.0 + Turborepo 单仓,落地 ESLint 9 flat、Prettier、commitlint、husky 与 Gitea CI,作为后续批次的一致门禁。
This commit is contained in:
1 parent
75cedbe97b
commit
b3a2a843bb
21 files changed
+10622
No files matched your search
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,116 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 'feat/**']
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: 质量门禁
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
env:
|
||||
POSTGRES_USER: lingxi
|
||||
POSTGRES_PASSWORD: lingxi_dev_password
|
||||
POSTGRES_DB: lingxi_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U lingxi -d lingxi_test"
|
||||
--health-interval 5s
|
||||
--health-timeout 3s
|
||||
--health-retries 10
|
||||
redis:
|
||||
image: redis:7.4-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 5s
|
||||
--health-timeout 3s
|
||||
--health-retries 10
|
||||
|
||||
env:
|
||||
NODE_ENV: test
|
||||
LOG_LEVEL: error
|
||||
DATABASE_URL: postgresql://lingxi:lingxi_dev_password@127.0.0.1:5432/lingxi_test?schema=public
|
||||
REDIS_URL: redis://127.0.0.1:6379
|
||||
JWT_SECRET: ci-only-access-secret
|
||||
JWT_REFRESH_SECRET: ci-only-refresh-secret
|
||||
WEB_ORIGIN: http://127.0.0.1:4173
|
||||
VITE_PUBLIC_API_BASE_URL: http://127.0.0.1:3000/api/v1
|
||||
VITE_PUBLIC_DEMO_SESSION: 'true'
|
||||
PLAYWRIGHT_CHANNEL: ''
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 安装 pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.34.9
|
||||
|
||||
- name: 安装 Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.15.0
|
||||
cache: pnpm
|
||||
|
||||
- name: 安装依赖
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 格式检查
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: 类型检查
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: 单元测试与覆盖率
|
||||
run: pnpm test
|
||||
|
||||
- name: 构建
|
||||
run: pnpm build
|
||||
|
||||
- name: 数据库迁移
|
||||
run: pnpm exec prisma migrate deploy
|
||||
|
||||
- name: 集成测试
|
||||
run: pnpm test:int
|
||||
|
||||
- name: 导出 OpenAPI 快照并检查破坏性变更
|
||||
run: |
|
||||
pnpm openapi:export
|
||||
pnpm openapi:diff
|
||||
|
||||
- name: 安装 Playwright 浏览器
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: 端到端冒烟
|
||||
run: pnpm test:e2e
|
||||
|
||||
- name: 归档测试产物
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-artifacts
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
coverage
|
||||
apps/*/coverage
|
||||
packages/*/coverage
|
||||
test-results
|
||||
playwright-report
|
||||
openapi/lingxi.openapi.json
|
||||
@@ -62,7 +62,16 @@ temp/
|
||||
~$*
|
||||
*.~lock.*
|
||||
|
||||
# ========== 测试产物 ==========
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# ========== 项目自定义 ==========
|
||||
# 参考项目:仅本地参照,不纳入版本控制、不推送到远程仓库
|
||||
参考项目/
|
||||
项目参考/
|
||||
# 沙箱环境下的本地 pnpm 10 启动器与设计探索产物
|
||||
.tools/
|
||||
design/
|
||||
# Vite 缓存
|
||||
.vite/
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm exec commitlint --edit "$1"
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm exec lint-staged
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm typecheck
|
||||
@@ -0,0 +1,18 @@
|
||||
const quote = (file) => JSON.stringify(file);
|
||||
|
||||
// 生成的声明文件(如 openapi.d.ts)被 ESLint 忽略,跳过 lint 只做格式化,
|
||||
// 避免 lint-staged 因 "File ignored" 警告而阻断提交。
|
||||
const lintScripts = (filenames) => {
|
||||
const lintable = filenames.filter((file) => !file.endsWith('.d.ts'));
|
||||
const commands = [];
|
||||
if (lintable.length > 0) {
|
||||
commands.push(`eslint --fix --max-warnings=0 ${lintable.map(quote).join(' ')}`);
|
||||
}
|
||||
commands.push(`prettier --write ${filenames.map(quote).join(' ')}`);
|
||||
return commands;
|
||||
};
|
||||
|
||||
export default {
|
||||
'*.{ts,tsx,js,mjs,cjs}': lintScripts,
|
||||
'*.{json,md,css,scss,yml,yaml}': ['prettier --write'],
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.turbo/
|
||||
.tools/
|
||||
design/
|
||||
docs/
|
||||
项目参考/
|
||||
openapi/
|
||||
prisma/migrations/
|
||||
pnpm-lock.yaml
|
||||
*.tsbuildinfo
|
||||
|
||||
# 项目总纲保持原排版,避免格式化产生无关 diff
|
||||
AGENTS.md
|
||||
@@ -0,0 +1,28 @@
|
||||
/** @type {import('@commitlint/types').UserConfig} */
|
||||
export default {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'docs',
|
||||
'style',
|
||||
'refactor',
|
||||
'perf',
|
||||
'test',
|
||||
'build',
|
||||
'ci',
|
||||
'chore',
|
||||
'revert',
|
||||
],
|
||||
],
|
||||
'subject-case': [0],
|
||||
'subject-empty': [2, 'never'],
|
||||
'header-max-length': [2, 'always', 72],
|
||||
'subject-full-stop': [2, 'never', '。'],
|
||||
'body-max-line-length': [0],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
// 灵犀 ESLint 9 flat config
|
||||
// 依据:docs/工程规范/03-格式化与Lint.md
|
||||
// 分层:根规则(全仓库)→ apps/web(React)→ packages/ui(图标统一出口)→ scripts(允许 console)
|
||||
import js from '@eslint/js';
|
||||
import importX from 'eslint-plugin-import-x';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const UI_LIB_MESSAGE = 'UI 组件库只能用 @douyinfe/semi-ui-19(工程规范 03 §3.2);禁装其他组件库。';
|
||||
const ICON_MESSAGE =
|
||||
'图标只能从 @lingxi/ui/icons 按语义名导入;禁止直接引用图标库或内联 SVG(AGENTS §3.3)。';
|
||||
const COLOR_MESSAGE =
|
||||
'业务代码禁止颜色字面量;颜色只能定义在 packages/ui/src/theme/ 的 token 中(工程规范 03 §3.2)。';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/coverage/**',
|
||||
'**/.turbo/**',
|
||||
'.tools/**',
|
||||
'design/**',
|
||||
'docs/**',
|
||||
'openapi/**',
|
||||
'prisma/migrations/**',
|
||||
'项目参考/**',
|
||||
'playwright-report/**',
|
||||
'test-results/**',
|
||||
'**/*.d.ts',
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
name: 'lingxi/base',
|
||||
languageOptions: {
|
||||
globals: { ...globals.node, ...globals.browser, ...globals.es2022 },
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'import-x': importX,
|
||||
'simple-import-sort': simpleImportSort,
|
||||
},
|
||||
settings: {
|
||||
'import-x/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json', './apps/*/tsconfig.json', './packages/*/tsconfig.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
linterOptions: { reportUnusedDisableDirectives: 'error' },
|
||||
rules: {
|
||||
// ---- TypeScript 硬约束(AGENTS §3.2)----
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', fixStyle: 'separate-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/restrict-template-expressions': [
|
||||
'error',
|
||||
{ allowNumber: true, allowBoolean: true },
|
||||
],
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||
{ selector: 'enumMember', format: ['UPPER_CASE'] },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
// ---- 导入与依赖方向 ----
|
||||
'import-x/no-cycle': ['error', { maxDepth: 8, ignoreExternal: false }],
|
||||
'import-x/no-self-import': 'error',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
// ---- 通用 ----
|
||||
'no-console': 'error',
|
||||
eqeqeq: ['error', 'always'],
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
complexity: ['warn', { max: 15 }],
|
||||
'max-lines-per-function': [
|
||||
'warn',
|
||||
{ max: 50, skipBlankLines: true, skipComments: true, IIFEs: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
// 纯 JS 配置文件不做类型检查
|
||||
{
|
||||
name: 'lingxi/js-config',
|
||||
files: ['**/*.{js,mjs,cjs}'],
|
||||
extends: [tseslint.configs.disableTypeChecked],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'import-x/no-cycle': 'off',
|
||||
},
|
||||
},
|
||||
// 应用层:Web(React 19)
|
||||
{
|
||||
name: 'lingxi/apps-web',
|
||||
files: ['apps/web/**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
'jsx-a11y': jsxA11y,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'jsx-a11y/alt-text': 'error',
|
||||
'jsx-a11y/no-autofocus': 'warn',
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{ group: ['@douyinfe/semi-ui', '@douyinfe/semi-ui/*'], message: UI_LIB_MESSAGE },
|
||||
{
|
||||
group: [
|
||||
'@remixicon/react',
|
||||
'react-icons',
|
||||
'react-icons/*',
|
||||
'lucide-react',
|
||||
'lucide',
|
||||
'semi-icons',
|
||||
'@douyinfe/semi-icons',
|
||||
'@heroicons/*',
|
||||
'@ant-design/icons',
|
||||
],
|
||||
message: ICON_MESSAGE,
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'antd',
|
||||
'@arco-design/web-react',
|
||||
'element-plus',
|
||||
'naive-ui',
|
||||
'@mui/*',
|
||||
'@chakra-ui/*',
|
||||
],
|
||||
message: UI_LIB_MESSAGE,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'Literal[value=/^#(?:[0-9a-fA-F]{3,8})$/]',
|
||||
message: COLOR_MESSAGE,
|
||||
},
|
||||
{
|
||||
selector: 'Literal[value=/^rgba?\\(/]',
|
||||
message: COLOR_MESSAGE,
|
||||
},
|
||||
{
|
||||
selector: "JSXOpeningElement[name.name='svg']",
|
||||
message: ICON_MESSAGE,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// packages/ui:图标统一出口(只有 icons.ts 可直接引用 Remix Icon)
|
||||
{
|
||||
name: 'lingxi/ui-icons',
|
||||
files: ['packages/ui/src/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{ patterns: [{ group: ['@remixicon/react'], message: ICON_MESSAGE }] },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lingxi/ui-icons-exit',
|
||||
files: ['packages/ui/src/icons.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-imports': ['error', { patterns: [] }],
|
||||
},
|
||||
},
|
||||
// Nest 模块装饰器类按框架约定是空类
|
||||
{
|
||||
name: 'lingxi/nest-modules',
|
||||
files: ['apps/api/**/*.module.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-extraneous-class': 'off',
|
||||
},
|
||||
},
|
||||
// 脚本层:允许 console(CLI 输出)
|
||||
{
|
||||
name: 'lingxi/scripts',
|
||||
files: ['scripts/**/*.{ts,mjs,js}', '**/scripts/**/*.ts', 'prisma/seed.ts', 'e2e/**/*.ts'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
// 测试文件:允许更松的表达
|
||||
{
|
||||
name: 'lingxi/tests',
|
||||
files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.int.spec.ts', 'e2e/**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'max-lines-per-function': 'off',
|
||||
complexity: 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "lingxi",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "灵犀 · 信息流广告素材一站式生产平台",
|
||||
"packageManager": "pnpm@10.34.9",
|
||||
"engines": {
|
||||
"node": "24.15.0",
|
||||
"pnpm": ">=10.0.0 <11.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/only-allow-pnpm.mjs",
|
||||
"prepare": "husky",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"build": "turbo run build",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "turbo run typecheck && tsc -p tsconfig.json --noEmit",
|
||||
"test": "turbo run test",
|
||||
"test:int": "turbo run test:int",
|
||||
"test:e2e": "turbo run build && playwright test",
|
||||
"compose:up": "docker compose -f infra/docker-compose.dev.yaml up -d --wait",
|
||||
"compose:down": "docker compose -f infra/docker-compose.dev.yaml down",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:reset": "prisma migrate reset --force",
|
||||
"db:seed": "tsx prisma/seed.ts",
|
||||
"openapi:export": "tsx --tsconfig apps/api/tsconfig.json apps/api/src/scripts/export-openapi.ts && openapi-typescript openapi/lingxi.openapi.json -o apps/web/src/api/generated/openapi.d.ts",
|
||||
"openapi:diff": "node scripts/openapi-diff.mjs",
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "20.5.3",
|
||||
"@commitlint/config-conventional": "20.5.3",
|
||||
"@eslint/js": "9.39.5",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@prisma/client": "6.19.3",
|
||||
"@types/node": "24.19.2",
|
||||
"@types/react": "19.3.0",
|
||||
"@types/react-dom": "19.3.0",
|
||||
"@vitest/coverage-v8": "3.2.7",
|
||||
"dotenv": "18.0.9",
|
||||
"eslint": "9.39.5",
|
||||
"eslint-import-resolver-typescript": "4.4.5",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"eslint-plugin-react-refresh": "0.5.7",
|
||||
"eslint-plugin-simple-import-sort": "14.0.0",
|
||||
"globals": "17.14.0",
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "16.4.0",
|
||||
"openapi-typescript": "7.13.0",
|
||||
"prettier": "3.9.10",
|
||||
"prisma": "6.19.3",
|
||||
"tsup": "8.5.1",
|
||||
"tsx": "4.23.15",
|
||||
"turbo": "2.11.10",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.72.1",
|
||||
"vitest": "3.2.7"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
Generated
+9932
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@prisma/client'
|
||||
- '@prisma/engines'
|
||||
- esbuild
|
||||
- prisma
|
||||
- sharp
|
||||
- unrs-resolver
|
||||
|
||||
overrides:
|
||||
fastify: 5.12.7
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
export default {
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'always',
|
||||
endOfLine: 'lf',
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
const userAgent = process.env.npm_config_user_agent ?? '';
|
||||
|
||||
if (!userAgent.startsWith('pnpm/')) {
|
||||
console.error('');
|
||||
console.error('✗ 灵犀项目只允许使用 pnpm 10 安装依赖(见 AGENTS.md §3)。');
|
||||
console.error(' 请执行:corepack enable && pnpm install');
|
||||
console.error(' 或使用:npx pnpm@10.34.9 install');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OpenAPI 兼容性检查(A0 回退实现)。
|
||||
*
|
||||
* 优先使用 oasdiff 二进制;未安装时使用本脚本的结构化回退检查:
|
||||
* - 删除 path / operation
|
||||
* - 删除 components.schemas
|
||||
* - 请求体必填字段新增、字段类型变更
|
||||
*
|
||||
* 基线来自 git HEAD 中的快照;首次提交(快照尚未入库)时跳过。
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const SNAPSHOT = resolve(process.cwd(), 'openapi/lingxi.openapi.json');
|
||||
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
||||
|
||||
if (!existsSync(SNAPSHOT)) {
|
||||
console.error(`未找到 OpenAPI 快照:${SNAPSHOT},请先执行 pnpm openapi:export`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const current = JSON.parse(readFileSync(SNAPSHOT, 'utf8'));
|
||||
|
||||
let baseline = null;
|
||||
try {
|
||||
const raw = execFileSync('git', ['show', `HEAD:${'openapi/lingxi.openapi.json'}`], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
baseline = JSON.parse(raw);
|
||||
} catch {
|
||||
console.log('基线快照尚未入库,跳过 OpenAPI diff(首次提交)。');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const breaking = [];
|
||||
|
||||
for (const [path, baselinePath] of Object.entries(baseline.paths ?? {})) {
|
||||
const currentPath = current.paths?.[path];
|
||||
if (!currentPath) {
|
||||
breaking.push(`删除路径:${path}`);
|
||||
continue;
|
||||
}
|
||||
for (const method of HTTP_METHODS) {
|
||||
if (baselinePath[method] !== undefined && currentPath[method] === undefined) {
|
||||
breaking.push(`删除操作:${method.toUpperCase()} ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const schemaName of Object.keys(baseline.components?.schemas ?? {})) {
|
||||
if (current.components?.schemas?.[schemaName] === undefined) {
|
||||
breaking.push(`删除 schema:${schemaName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const baselineSchemas = baseline.components?.schemas ?? {};
|
||||
const currentSchemas = current.components?.schemas ?? {};
|
||||
for (const [name, baselineSchema] of Object.entries(baselineSchemas)) {
|
||||
const currentSchema = currentSchemas[name];
|
||||
if (currentSchema === undefined) {
|
||||
continue;
|
||||
}
|
||||
const baselineRequired = new Set(baselineSchema.required ?? []);
|
||||
for (const required of currentSchema.required ?? []) {
|
||||
if (!baselineRequired.has(required)) {
|
||||
breaking.push(`schema ${name} 新增必填字段:${required}`);
|
||||
}
|
||||
}
|
||||
for (const [property, baselineProperty] of Object.entries(baselineSchema.properties ?? {})) {
|
||||
const currentProperty = currentSchema.properties?.[property];
|
||||
if (
|
||||
currentProperty !== undefined &&
|
||||
baselineProperty.type !== undefined &&
|
||||
currentProperty.type !== undefined &&
|
||||
baselineProperty.type !== currentProperty.type
|
||||
) {
|
||||
breaking.push(
|
||||
`schema ${name}.${property} 类型变更:${baselineProperty.type} -> ${currentProperty.type}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (breaking.length > 0) {
|
||||
console.error('检测到破坏性 OpenAPI 变更:');
|
||||
for (const item of breaking) {
|
||||
console.error(` - ${item}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('OpenAPI 无破坏性变更。');
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["playwright.config.ts", "scripts/**/*.ts", "e2e/**/*.ts", "prisma/seed.ts"]
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "stream",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"typecheck": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["coverage/**"]
|
||||
},
|
||||
"test:int": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user