背景:Gitea 暂未部署 Actions runner,远程 CI 会停留在 queued;本地开发库按约定改用 lingxi / postgres / postgres。 改动: - 新增 pnpm ci:local,按 CI 工作流顺序执行 compose、格式、Lint、类型、单测、构建、迁移种子、集成、OpenAPI 与 e2e; - 新增 scripts/e2e.mjs 管理 vite preview 生命周期,e2e 改为单 worker 避免退出挂起; - openapi:export 生成后自动 Prettier,保持工作区干净; - compose、.env.example、CI services 数据库凭据统一为 postgres/postgres,库名 lingxi; - README、AGENTS、A0 报告同步本地 CI 与数据库说明。 影响面:仅本地开发与 CI 执行方式,无业务代码/接口/数据模型变更;本地库已重建为 lingxi/postgres。 回滚方式:git revert 本提交;数据库需按回滚后的 compose 凭据重新初始化。 验证:pnpm ci:local 全绿(158s),format:check、lint、typecheck、openapi:diff 通过。
This commit is contained in:
1 parent
e35637cfd7
commit
68b5ca20f4
10 files changed
+137
-24
No files matched your search
+1
-1
@@ -7,7 +7,7 @@ NODE_ENV=development
|
|||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
# ---------- 数据库(PostgreSQL 18) ----------
|
# ---------- 数据库(PostgreSQL 18) ----------
|
||||||
DATABASE_URL=postgresql://lingxi:lingxi_dev_password@127.0.0.1:15432/lingxi_dev?schema=public
|
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:15432/lingxi?schema=public
|
||||||
|
|
||||||
# ---------- Redis(7.4) ----------
|
# ---------- Redis(7.4) ----------
|
||||||
REDIS_URL=redis://127.0.0.1:16379
|
REDIS_URL=redis://127.0.0.1:16379
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ jobs:
|
|||||||
postgres:
|
postgres:
|
||||||
image: postgres:18-alpine
|
image: postgres:18-alpine
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: lingxi
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: lingxi_dev_password
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: lingxi_test
|
POSTGRES_DB: lingxi_test
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
LOG_LEVEL: error
|
LOG_LEVEL: error
|
||||||
DATABASE_URL: postgresql://lingxi:lingxi_dev_password@127.0.0.1:5432/lingxi_test?schema=public
|
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/lingxi_test?schema=public
|
||||||
REDIS_URL: redis://127.0.0.1:6379
|
REDIS_URL: redis://127.0.0.1:6379
|
||||||
JWT_SECRET: ci-only-access-secret
|
JWT_SECRET: ci-only-access-secret
|
||||||
JWT_REFRESH_SECRET: ci-only-refresh-secret
|
JWT_REFRESH_SECRET: ci-only-refresh-secret
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ docs/ PRD / 技术架构 / 立项建议 / 工程规范
|
|||||||
| 开发 | `pnpm dev` |
|
| 开发 | `pnpm dev` |
|
||||||
| 构建 | `pnpm build` |
|
| 构建 | `pnpm build` |
|
||||||
| 质量 | `pnpm lint` · `pnpm lint:fix` · `pnpm format` · `pnpm format:check` · `pnpm typecheck` |
|
| 质量 | `pnpm lint` · `pnpm lint:fix` · `pnpm format` · `pnpm format:check` · `pnpm typecheck` |
|
||||||
| 测试 | `pnpm test` · `pnpm test:int` · `pnpm test:e2e` |
|
| 测试 | `pnpm test` · `pnpm test:int` · `pnpm test:e2e` · `pnpm ci:local` |
|
||||||
| 数据库 | `pnpm db:migrate` · `pnpm db:reset`(禁止 `docker compose down -v`) |
|
| 数据库 | `pnpm db:migrate` · `pnpm db:reset`(禁止 `docker compose down -v`) |
|
||||||
| 契约 | `pnpm openapi:diff` |
|
| 契约 | `pnpm openapi:diff` |
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ pnpm db:seed # 幂等写入 system_configs 基线
|
|||||||
pnpm dev # 并行启动 api(3000) / web(5173) / worker
|
pnpm dev # 并行启动 api(3000) / web(5173) / worker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **本地数据库**:库名 `lingxi`,用户 `postgres`,密码 `postgres`(仅开发环境,见 `infra/docker-compose.dev.yaml` 与 `.env.example`)。
|
||||||
- **端口**:为避免与本机已有容器冲突,compose 宿主端口为 PostgreSQL `15432`、Redis `16379`、S3 `19000/19001`,容器内仍是 `5432/6379/9000/9001`;`.env.example` 已按宿主端口给出连接串。
|
- **端口**:为避免与本机已有容器冲突,compose 宿主端口为 PostgreSQL `15432`、Redis `16379`、S3 `19000/19001`,容器内仍是 `5432/6379/9000/9001`;`.env.example` 已按宿主端口给出连接串。
|
||||||
- **对象存储**:当前 Docker 环境拉取不到 `minio/minio` 镜像(registry denied、二进制 410),A0 的 compose 使用 `adobe/s3mock` 并提供 `assets / renders / temp / archive` 四个初始桶;CI 或生产接入真实 MinIO 时只需替换 `infra/docker-compose.dev.yaml` 的 `s3` 服务。
|
- **对象存储**:当前 Docker 环境拉取不到 `minio/minio` 镜像(registry denied、二进制 410),A0 的 compose 使用 `adobe/s3mock` 并提供 `assets / renders / temp / archive` 四个初始桶;CI 或生产接入真实 MinIO 时只需替换 `infra/docker-compose.dev.yaml` 的 `s3` 服务。
|
||||||
- **API 文档**:启动 api 后访问 `http://127.0.0.1:3000/api/docs`,OpenAPI JSON 在 `openapi/lingxi.openapi.json`。
|
- **API 文档**:启动 api 后访问 `http://127.0.0.1:3000/api/docs`,OpenAPI JSON 在 `openapi/lingxi.openapi.json`。
|
||||||
@@ -90,8 +91,11 @@ pnpm dev # 并行启动 api(3000) / web(5173) / worker
|
|||||||
pnpm lint && pnpm typecheck && pnpm test # 本地门禁
|
pnpm lint && pnpm typecheck && pnpm test # 本地门禁
|
||||||
pnpm build && pnpm test:e2e # 构建与端到端冒烟
|
pnpm build && pnpm test:e2e # 构建与端到端冒烟
|
||||||
pnpm openapi:export && pnpm openapi:diff # 契约快照与破坏性变更检查
|
pnpm openapi:export && pnpm openapi:diff # 契约快照与破坏性变更检查
|
||||||
|
pnpm ci:local # 本地跑完整 CI 门禁
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Gitea 暂未部署 Actions runner,推送后远程 CI 会停留在 queued;当前以 `pnpm ci:local` 为准,runner 就绪后 `.gitea/workflows/ci.yml` 会自动接管。
|
||||||
|
|
||||||
命令与门禁见 [AGENTS.md §6](AGENTS.md) 与 [06-测试与CI规范](docs/工程规范/06-测试与CI规范.md)。当前 UI 采用「顶部模块菜单 + 左侧功能菜单」布局,视觉方向为 **A · 云白蓝(传统企业后台 + 现代浅色质感)**。
|
命令与门禁见 [AGENTS.md §6](AGENTS.md) 与 [06-测试与CI规范](docs/工程规范/06-测试与CI规范.md)。当前 UI 采用「顶部模块菜单 + 左侧功能菜单」布局,视觉方向为 **A · 云白蓝(传统企业后台 + 现代浅色质感)**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| A0-INF-01 Monorepo / 工具链 | 完成 | pnpm workspace + Turborepo;TS strict、ESLint 9 flat + Prettier + commitlint + lint-staged + husky;`.nvmrc` / `engines` / `packageManager` / preinstall 仅允许 pnpm;目录与 AGENTS.md §5 一致 |
|
| A0-INF-01 Monorepo / 工具链 | 完成 | pnpm workspace + Turborepo;TS strict、ESLint 9 flat + Prettier + commitlint + lint-staged + husky;`.nvmrc` / `engines` / `packageManager` / preinstall 仅允许 pnpm;目录与 AGENTS.md §5 一致 |
|
||||||
| A0-INF-02 本地依赖与迁移种子 | 完成(种子范围受限,见 §4.4) | `infra/docker-compose.dev.yaml`(PG 18 / Redis 7.4 / S3 兼容服务)+ 健康检查;`.env.example`;Prisma 初始化与首版迁移;`system_configs` 幂等种子 |
|
| A0-INF-02 本地依赖与迁移种子 | 完成(种子范围受限,见 §4.4) | `infra/docker-compose.dev.yaml`(PG 18 / Redis 7.4 / S3 兼容服务)+ 健康检查;`.env.example`;Prisma 初始化与首版迁移;`system_configs` 幂等种子 |
|
||||||
| A0-INF-03 CI 与质量门禁 | 完成 | `.gitea/workflows/ci.yml`:install → format → lint → typecheck → test → build → migrate → test:int → openapi:export/diff → e2e;产物归档;失败阻断 |
|
| A0-INF-03 CI 与质量门禁 | 完成 | `.gitea/workflows/ci.yml`:install → format → lint → typecheck → test → build → migrate → test:int → openapi:export/diff → e2e;新增 `pnpm ci:local` 在无 runner 时本地执行同一门禁;失败阻断 |
|
||||||
| A0-BE-01 API 骨架 | 完成 | NestJS 11 + Fastify;Pino + `trace_id`;统一错误体 `{code,message,details,trace_id}`;`GET /health`;`/api/docs` 与 OpenAPI JSON;Zod 管道;CORS / Helmet |
|
| A0-BE-01 API 骨架 | 完成 | NestJS 11 + Fastify;Pino + `trace_id`;统一错误体 `{code,message,details,trace_id}`;`GET /health`;`/api/docs` 与 OpenAPI JSON;Zod 管道;CORS / Helmet |
|
||||||
| A0-BE-02 Prisma 基线 | 完成 | `PrismaService` 封装;`outbox_events` / `idempotency_keys` / `system_configs`;ULID `char(26)`、软删/时间字段约定;首版迁移可重复执行 |
|
| A0-BE-02 Prisma 基线 | 完成 | `PrismaService` 封装;`outbox_events` / `idempotency_keys` / `system_configs`;ULID `char(26)`、软删/时间字段约定;首版迁移可重复执行 |
|
||||||
| A0-SH-01 shared 包 | 完成 | 20 个错误码(40001–50003)、权限点常量与 `模块:动作` 规范、ULID、游标分页、时间/命名工具、通用 Zod schema;前后端共同引用 |
|
| A0-SH-01 shared 包 | 完成 | 20 个错误码(40001–50003)、权限点常量与 `模块:动作` 规范、ULID、游标分页、时间/命名工具、通用 Zod schema;前后端共同引用 |
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
### 4.1 宿主端口(环境适配)
|
### 4.1 宿主端口(环境适配)
|
||||||
|
|
||||||
本机已有容器占用 5432 / 6379,A0 compose 宿主端口改为 **15432(PG)/ 16379(Redis)/ 19000–19001(S3)**,容器内端口不变。`.env.example` 已按宿主端口提供连接串;CI 使用 GitHub Actions services 的标准端口。
|
本机已有容器占用 5432 / 6379,A0 compose 宿主端口改为 **15432(PG)/ 16379(Redis)/ 19000–19001(S3)**,容器内端口不变。`.env.example` 已按宿主端口提供连接串;本地数据库为 `lingxi`,用户 `postgres`,密码 `postgres`(仅开发环境)。CI services 使用标准端口,凭据同为 `postgres/postgres`。
|
||||||
|
|
||||||
### 4.2 对象存储镜像不可得(环境适配)
|
### 4.2 对象存储镜像不可得(环境适配)
|
||||||
|
|
||||||
@@ -76,6 +76,12 @@ Prisma CLI 只自动读取 `.env`,不读取 `.env.local`。本地需执行 `cp
|
|||||||
|
|
||||||
npm 上 `oasdiff` 为占位包(0.0.1-security),A0 暂用自研 `scripts/openapi-diff.mjs` 做路径/操作/字段级破坏性检查;CI 可后续替换为 oasdiff 官方二进制,命令仍为 `pnpm openapi:diff`。
|
npm 上 `oasdiff` 为占位包(0.0.1-security),A0 暂用自研 `scripts/openapi-diff.mjs` 做路径/操作/字段级破坏性检查;CI 可后续替换为 oasdiff 官方二进制,命令仍为 `pnpm openapi:diff`。
|
||||||
|
|
||||||
|
### 4.9 本地 CI 与数据库凭据
|
||||||
|
|
||||||
|
当前离线 Gitea 未部署 Actions runner,推送后远程 CI 会停留在 `queued`。A0 增加 `pnpm ci:local`:按 `.gitea/workflows/ci.yml` 的顺序在本地执行 compose、格式、Lint、类型、单测、构建、迁移与种子、集成、OpenAPI 契约与 e2e,全部通过才退出 0;runner 就绪后远程工作流可直接接管。
|
||||||
|
|
||||||
|
本地开发数据库已按约定改为:库名 `lingxi`、用户 `postgres`、密码 `postgres`;compose、`.env.example`、CI services 三处保持一致。
|
||||||
|
|
||||||
### 4.8 前端视觉方向
|
### 4.8 前端视觉方向
|
||||||
|
|
||||||
已按 oil-ui-pro 流程产出两轮方案:
|
已按 oil-ui-pro 流程产出两轮方案:
|
||||||
@@ -94,7 +100,7 @@ npm 上 `oasdiff` 为占位包(0.0.1-security),A0 暂用自研 `scripts/op
|
|||||||
2. 种子业务数据待 A1 / A3 / A4 / B0 表就绪后补齐(Q-A0-01)。
|
2. 种子业务数据待 A1 / A3 / A4 / B0 表就绪后补齐(Q-A0-01)。
|
||||||
3. MinIO 本地替代方案需在具备镜像的环境回归;生产 compose 与 CI 镜像另行处理。
|
3. MinIO 本地替代方案需在具备镜像的环境回归;生产 compose 与 CI 镜像另行处理。
|
||||||
4. Web 主包约 599 kB(Semi 全量 CSS 689 kB),A0 接受;M1 前做路由级拆分与 Semi 按需样式。
|
4. Web 主包约 599 kB(Semi 全量 CSS 689 kB),A0 接受;M1 前做路由级拆分与 Semi 按需样式。
|
||||||
5. CI 首次运行需在 Gitea Actions 上验证 runner 是否支持 services 与 Playwright 浏览器安装。
|
5. 远程 CI 待 Gitea Actions runner 就绪后验证 services 与 Playwright 浏览器安装;当前以 `pnpm ci:local` 为准(已实测通过)。
|
||||||
|
|
||||||
## 6. 下一步(A1)
|
## 6. 下一步(A1)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ services:
|
|||||||
image: postgres:18-alpine
|
image: postgres:18-alpine
|
||||||
container_name: lingxi-pg
|
container_name: lingxi-pg
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: lingxi
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: lingxi_dev_password
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: lingxi_dev
|
POSTGRES_DB: lingxi
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
ports:
|
ports:
|
||||||
- '15432:5432'
|
- '15432:5432'
|
||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
# PG18+ 镜像要求挂载 /var/lib/postgresql(数据放在版本子目录,便于 pg_upgrade)
|
# PG18+ 镜像要求挂载 /var/lib/postgresql(数据放在版本子目录,便于 pg_upgrade)
|
||||||
- lingxi-pg-data:/var/lib/postgresql
|
- lingxi-pg-data:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'pg_isready -U lingxi -d lingxi_dev']
|
test: ['CMD-SHELL', 'pg_isready -U postgres -d lingxi']
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|||||||
+3
-2
@@ -20,14 +20,15 @@
|
|||||||
"typecheck": "turbo run typecheck && tsc -p tsconfig.json --noEmit",
|
"typecheck": "turbo run typecheck && tsc -p tsconfig.json --noEmit",
|
||||||
"test": "turbo run test",
|
"test": "turbo run test",
|
||||||
"test:int": "turbo run test:int",
|
"test:int": "turbo run test:int",
|
||||||
"test:e2e": "turbo run build && playwright test",
|
"test:e2e": "turbo run build && node scripts/e2e.mjs",
|
||||||
|
"ci:local": "node scripts/ci-local.mjs",
|
||||||
"compose:up": "docker compose -f infra/docker-compose.dev.yaml up -d --wait",
|
"compose:up": "docker compose -f infra/docker-compose.dev.yaml up -d --wait",
|
||||||
"compose:down": "docker compose -f infra/docker-compose.dev.yaml down",
|
"compose:down": "docker compose -f infra/docker-compose.dev.yaml down",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:reset": "prisma migrate reset --force",
|
"db:reset": "prisma migrate reset --force",
|
||||||
"db:seed": "tsx prisma/seed.ts",
|
"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: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 && prettier --write apps/web/src/api/generated/openapi.d.ts",
|
||||||
"openapi:diff": "node scripts/openapi-diff.mjs",
|
"openapi:diff": "node scripts/openapi-diff.mjs",
|
||||||
"postinstall": "prisma generate"
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-10
@@ -6,7 +6,8 @@ export default defineConfig({
|
|||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
expect: { timeout: 10_000 },
|
expect: { timeout: 10_000 },
|
||||||
fullyParallel: true,
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
forbidOnly: Boolean(process.env.CI),
|
forbidOnly: Boolean(process.env.CI),
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
reporter: process.env.CI
|
reporter: process.env.CI
|
||||||
@@ -24,13 +25,4 @@ export default defineConfig({
|
|||||||
use: { ...devices['Desktop Chrome'], channel },
|
use: { ...devices['Desktop Chrome'], channel },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
webServer: {
|
|
||||||
command: 'node node_modules/vite/bin/vite.js preview --host 127.0.0.1 --port 4173 --strictPort',
|
|
||||||
cwd: 'apps/web',
|
|
||||||
url: 'http://127.0.0.1:4173/login',
|
|
||||||
reuseExistingServer: false,
|
|
||||||
timeout: 60_000,
|
|
||||||
stdout: 'ignore',
|
|
||||||
stderr: 'pipe',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 本地 CI:在无 Gitea Actions runner 时,按 .gitea/workflows/ci.yml 的门禁顺序执行。
|
||||||
|
* 用法:pnpm ci:local(推荐通过 ./.tools/pnpm ci:local 锁定 pnpm 10)
|
||||||
|
*/
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
const pnpm = process.env.npm_execpath ? [process.execPath, process.env.npm_execpath] : ['pnpm'];
|
||||||
|
|
||||||
|
/** 每一步可包含多条 pnpm 命令,任一失败立即退出。 */
|
||||||
|
const steps = [
|
||||||
|
{ label: '启动本地依赖(PG / Redis / S3)', commands: [['compose:up']] },
|
||||||
|
{ label: '格式化检查', commands: [['format:check']] },
|
||||||
|
{ label: 'Lint', commands: [['lint']] },
|
||||||
|
{ label: '类型检查', commands: [['typecheck']] },
|
||||||
|
{ label: '单元测试', commands: [['test']] },
|
||||||
|
{ label: '构建', commands: [['build']] },
|
||||||
|
{
|
||||||
|
label: '数据库迁移与种子',
|
||||||
|
commands: [['exec', 'prisma', 'migrate', 'deploy'], ['db:seed']],
|
||||||
|
},
|
||||||
|
{ label: '集成测试', commands: [['test:int']] },
|
||||||
|
{ label: 'OpenAPI 导出与契约检查', commands: [['openapi:export'], ['openapi:diff']] },
|
||||||
|
{ label: '端到端测试', commands: [['test:e2e']] },
|
||||||
|
];
|
||||||
|
|
||||||
|
function runPnpm(args) {
|
||||||
|
const result = spawnSync(pnpm[0], [...pnpm.slice(1), ...args], {
|
||||||
|
env: process.env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
console.error(`\n执行 pnpm ${args.join(' ')} 失败:${result.error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (result.status !== 0) {
|
||||||
|
console.error(`\n门禁失败:pnpm ${args.join(' ')}(exit ${result.status ?? 1})`);
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedAt = Date.now();
|
||||||
|
for (const [index, step] of steps.entries()) {
|
||||||
|
console.log(`\n[${index + 1}/${steps.length}] ${step.label}`);
|
||||||
|
for (const args of step.commands) {
|
||||||
|
runPnpm(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const seconds = ((Date.now() - startedAt) / 1000).toFixed(1);
|
||||||
|
console.log(`\n本地 CI 全部门禁通过,用时 ${seconds}s。`);
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 端到端测试启动器:先起 vite preview,再跑 Playwright,结束后确保关闭预览服务。
|
||||||
|
* 不使用 Playwright webServer,避免 vite preview 在部分环境下退出挂起。
|
||||||
|
* 用法:pnpm test:e2e(会先执行 build)
|
||||||
|
*/
|
||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { setTimeout as delay } from 'node:timers/promises';
|
||||||
|
|
||||||
|
const baseUrl = 'http://127.0.0.1:4173';
|
||||||
|
const pnpm = process.env.npm_execpath ? [process.execPath, process.env.npm_execpath] : ['pnpm'];
|
||||||
|
|
||||||
|
function runPnpm(args, options = {}) {
|
||||||
|
return spawnSync(pnpm[0], [...pnpm.slice(1), ...args], {
|
||||||
|
env: process.env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = spawn(pnpm[0], [...pnpm.slice(1), '--filter', '@lingxi/web', 'preview'], {
|
||||||
|
env: process.env,
|
||||||
|
stdio: 'ignore',
|
||||||
|
detached: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function waitForServer(url, timeoutMs = 60_000) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) return;
|
||||||
|
} catch {
|
||||||
|
// 预览服务尚未就绪,继续重试。
|
||||||
|
}
|
||||||
|
await delay(500);
|
||||||
|
}
|
||||||
|
throw new Error(`预览服务在 ${timeoutMs}ms 内未就绪:${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let exitCode = 1;
|
||||||
|
try {
|
||||||
|
await waitForServer(`${baseUrl}/login`);
|
||||||
|
const result = runPnpm(['exec', 'playwright', 'test']);
|
||||||
|
exitCode = result.status ?? 1;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
process.kill(-server.pid, 'SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// 进程可能已退出。
|
||||||
|
}
|
||||||
|
await delay(1_000);
|
||||||
|
try {
|
||||||
|
process.kill(-server.pid, 'SIGKILL');
|
||||||
|
} catch {
|
||||||
|
// 进程可能已退出。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(exitCode);
|
||||||
Reference in new issue
Block a user