feat(ci): 搭建 A0 工程骨架与契约基线 #2
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.*
|
*.~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