Files
yunrui_asset/install/installer.js
T
yi e9bff9beb3
Build and Push Docker Images / Build Backend Image (push) Has been cancelled
Build and Push Docker Images / Build Frontend Image (push) Has been cancelled
Build and Push Docker Images / Notify Build Complete (push) Has been cancelled
Update 2026-06-23 10:40:10
2026-06-23 10:40:10 +08:00

243 lines
7.8 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { log } = require('./logger');
const { runCommand } = require('./utils');
const { config } = require('./config');
const { rollbackSteps } = require('./rollback');
const { NPM_MIRRORS } = require('./constants');
function checkRegistryReachable(registry, timeout = 10000) {
return new Promise((resolve) => {
const url = new URL(registry);
const http = url.protocol === 'https:' ? require('https') : require('http');
const req = http.get(`${registry}/-/ping`, { timeout }, (res) => {
res.resume();
resolve(res.statusCode >= 200 && res.statusCode < 500);
});
req.on('error', () => resolve(false));
req.on('timeout', () => { req.destroy(); resolve(false); });
});
}
async function detectAvailableRegistry() {
for (const mirror of NPM_MIRRORS) {
log.subStep(`检测 ${mirror.name} (${mirror.registry})...`);
const reachable = await checkRegistryReachable(mirror.registry);
if (reachable) {
log.success(`${mirror.name} 可用`);
return mirror.registry;
}
log.subStep(`${mirror.name} 不可达`);
}
return NPM_MIRRORS[0].registry;
}
async function npmInstallWithRetry(cwd, label) {
const maxRetries = 3;
const baseDelay = 3000;
const availableRegistry = await detectAvailableRegistry();
const isDefaultRegistry = availableRegistry === NPM_MIRRORS[0].registry;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
if (attempt > 1) {
const delay = baseDelay * Math.pow(2, attempt - 2);
log.subStep(`第 ${attempt}/${maxRetries} 次重试,${delay / 1000}秒后...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
let command = 'npm install';
if (!isDefaultRegistry) {
command += ` --registry=${availableRegistry}`;
}
log.info(`安装${label}依赖${attempt > 1 ? ` (第${attempt}次尝试)` : ''}...`);
const result = runCommand(command, { cwd });
if (result.success) {
const nodeModulesPath = path.join(cwd, 'node_modules');
if (fs.existsSync(nodeModulesPath)) {
return { success: true };
}
log.subStep(`${label} node_modules 不存在,安装可能未完成`);
} else {
log.subStep(`${label}依赖安装失败: ${result.error || '未知错误'}`);
}
}
return { success: false, error: `${label}依赖安装失败(已重试${maxRetries}次)` };
}
async function installDependencies() {
log.step('安装依赖');
const backendDir = path.join(__dirname, '..', 'backend');
const frontendDir = path.join(__dirname, '..', 'frontend');
log.info('安装后端依赖...');
const backendResult = await npmInstallWithRetry(backendDir, '后端');
if (backendResult.success) {
log.success('后端依赖安装完成');
rollbackSteps.push(() => {
const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules';
runCommand(rmCmd, { cwd: backendDir, silent: true });
});
} else {
log.error(backendResult.error);
throw new Error('Backend install failed');
}
log.info('安装前端依赖...');
const frontendResult = await npmInstallWithRetry(frontendDir, '前端');
if (frontendResult.success) {
log.success('前端依赖安装完成');
rollbackSteps.push(() => {
const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules';
runCommand(rmCmd, { cwd: frontendDir, silent: true });
});
} else {
log.error(frontendResult.error);
throw new Error('Frontend install failed');
}
}
async function initDatabase() {
log.step('数据库初始化');
const initScript = path.join(__dirname, '..', 'backend', 'scripts', 'init-database.js');
if (!fs.existsSync(initScript)) {
log.warning('未找到数据库初始化脚本,跳过');
return;
}
log.info('正在初始化数据库...');
const result = runCommand('node scripts/init-database.js', {
cwd: path.join(__dirname, '..', 'backend')
});
if (result.success) {
log.success('数据库初始化完成');
} else {
log.error('数据库初始化失败');
log.info('请检查数据库配置并手动运行: node backend/scripts/init-database.js');
throw new Error('Database initialization failed');
}
}
function getAvailableMemoryMB() {
try {
const os = require('os');
return Math.round(os.freemem() / 1024 / 1024);
} catch {
return null;
}
}
async function buildFrontend() {
log.step('构建前端');
const frontendDir = path.join(__dirname, '..', 'frontend');
const freeMemMB = getAvailableMemoryMB();
let buildCommand = 'npm run build';
if (freeMemMB !== null) {
if (freeMemMB < 512) {
log.warning(`系统可用内存较低 (${freeMemMB}MB),前端构建可能失败`);
log.info('建议增加内存或创建 swap 分区后再试');
} else if (freeMemMB < 1024) {
log.info(`可用内存 ${freeMemMB}MB,限制 Node 堆栈为 512MB...`);
buildCommand = 'node --max_old_space_size=512 node_modules/vite/bin/vite.js build';
}
}
log.info('执行前端构建...');
const result = runCommand(buildCommand, { cwd: frontendDir });
if (result.success) {
log.success('前端构建完成');
rollbackSteps.push(() => {
const distDir = path.join(frontendDir, 'dist');
if (fs.existsSync(distDir)) {
const rmCmd = process.platform === 'win32' ? 'rmdir /s /q dist' : 'rm -rf dist';
runCommand(rmCmd, { cwd: frontendDir, silent: true });
}
});
} else {
log.error('前端构建失败');
throw new Error('Frontend build failed');
}
}
async function startServices() {
log.step('启动服务');
const logDir = path.join(__dirname, '..', 'backend', 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
log.info('停止已有服务...');
const stopCmd = process.platform === 'win32'
? 'pm2 stop yunrui_asset-backend yunrui_asset-frontend 2>nul || exit 0'
: 'pm2 stop yunrui_asset-backend yunrui_asset-frontend 2>/dev/null || true';
runCommand(stopCmd, { silent: true });
log.info('启动后端服务...');
const backendResult = runCommand(
`pm2 start server.js --name "yunrui_asset-backend" --env ${config.nodeEnv}`,
{ cwd: path.join(__dirname, '..', 'backend') }
);
if (backendResult.success) {
log.success(`后端服务已启动 (端口: ${config.backendPort})`);
rollbackSteps.push(() => {
runCommand('pm2 stop yunrui_asset-backend', { silent: true });
runCommand('pm2 delete yunrui_asset-backend', { silent: true });
});
} else {
log.error('后端服务启动失败');
log.info('请检查后端日志: pm2 logs yunrui_asset-backend');
throw new Error('Backend service start failed');
}
if (config.frontendDeploy === 'pm2') {
log.info('安装 serve 包...');
runCommand('npm install -g serve', { silent: true });
log.info('启动前端服务...');
const frontendResult = runCommand(
`pm2 start serve --name "yunrui_asset-frontend" -- -s dist -l ${config.frontendPort}`,
{ cwd: path.join(__dirname, '..', 'frontend') }
);
if (frontendResult.success) {
log.success(`前端服务已启动 (端口: ${config.frontendPort})`);
rollbackSteps.push(() => {
runCommand('pm2 stop yunrui_asset-frontend', { silent: true });
runCommand('pm2 delete yunrui_asset-frontend', { silent: true });
});
} else {
log.error('前端服务启动失败');
log.info('请检查前端日志: pm2 logs yunrui_asset-frontend');
throw new Error('Frontend service start failed');
}
}
runCommand('pm2 save', { silent: true });
log.divider();
}
module.exports = {
checkRegistryReachable,
detectAvailableRegistry,
npmInstallWithRetry,
installDependencies,
initDatabase,
getAvailableMemoryMB,
buildFrontend,
startServices,
};