import axios from 'axios'; import dotenv from 'dotenv'; import type { CommonOptions } from 'execa'; import execa from 'execa'; import _ from 'lodash'; import net from 'net'; import fs from 'node:fs'; import path from 'path'; const PORT = process.env.APP_PORT || 20000; export const APP_BASE_URL = process.env.APP_BASE_URL || `http://localhost:${PORT}`; export const commonConfig: any = { stdio: 'inherit', }; export const runCommand = (command, argv, options: any = {}) => { return execa(command, argv, { shell: true, stdio: 'inherit', ..._.omit(options, 'force'), env: { ...process.env, }, }); }; /** * 检查端口是否被占用 * @param port * @returns */ function checkPort(port) { return new Promise((resolve, reject) => { const socket = net.createConnection(port, '127.0.0.1'); socket.on('connect', () => { socket.destroy(); resolve(true); // 端口可用 }); socket.on('error', (error) => { resolve(false); // 端口被占用或不可访问 }); }); } /** * 检查服务是否启动成功 */ const checkServer = async (duration = 1000, max = 60 * 10) => { return new Promise((resolve, reject) => { let count = 0; const timer = setInterval(async () => { if (count++ > max) { clearInterval(timer); return reject(new Error('Server start timeout.')); } if (!(await checkPort(PORT))) { return; } axios .get(`${APP_BASE_URL}/api/__health_check`) .then((response) => { if (response.status === 200) { clearInterval(timer); resolve(true); } }) .catch((error) => { console.error('Request error:', error.message); }); }, duration); }); }; /** * 检查 UI 是否启动成功 * @param duration */ const checkUI = async (duration = 1000, max = 60 * 10) => { return new Promise((resolve, reject) => { let count = 0; const timer = setInterval(async () => { if (count++ > max) { clearInterval(timer); return reject(new Error('UI start timeout.')); } axios .get(`${APP_BASE_URL}/__umi/api/bundle-status`) .then((response) => { if (response.data.bundleStatus.done) { clearInterval(timer); resolve(true); } }) .catch((error) => { console.error('Request error:', error.message); }); }, duration); }); }; export const runNocoBase = async ( options?: CommonOptions & { /** * 是否强制启动服务 */ force?: boolean; signal?: AbortSignal; }, clearDatabase = false, ) => { // 用于存放 playwright 自动生成的相关的文件 if (!fs.existsSync('playwright')) { fs.mkdirSync('playwright'); } if (!fs.existsSync('.env.e2e') && fs.existsSync('.env.e2e.example')) { const env = fs.readFileSync('.env.e2e.example'); fs.writeFileSync('.env.e2e', env); } if (!fs.existsSync('.env.e2e')) { throw new Error('Please create .env.e2e file first!'); } dotenv.config({ path: path.resolve(process.cwd(), '.env.e2e') }); if (process.env.APP_BASE_URL && !options?.force) { console.log('APP_BASE_URL is setting, skip starting server.'); return { awaitForNocoBase: () => {} }; } const awaitForNocoBase = async () => { if (process.env.CI) { console.log('check server...'); await checkServer(); } else { console.log('check server...'); await checkServer(); console.log('server is ready, check UI...'); await checkUI(); console.log('UI is ready.'); } }; if (process.env.CI) { console.log('yarn nocobase install'); await runCommand('yarn', ['nocobase', 'install'], options); console.log(`yarn start -d -p ${PORT}`); await runCommand('yarn', ['start', '-d', `-p ${PORT}`], options); return { awaitForNocoBase }; } if (clearDatabase) { // 加上 -f 会清空数据库 console.log('yarn nocobase install -f'); await runCommand('yarn', ['nocobase', 'install', '-f'], options); } if (await checkPort(PORT)) { console.log('Server is running, skip starting server.'); return { awaitForNocoBase }; } console.log('starting server...'); const { cancel, kill } = runCommand('yarn', ['dev', `-p ${PORT}`, ...process.argv.slice(2)], options); return { cancel, kill, awaitForNocoBase }; };