#!/usr/bin/env node /** * 总部 H5 静态预览:托管 dist 并将 /api 代理到后端(与 admin-web vite proxy 行为一致) */ import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { execSync } from 'node:child_process'; import { platform } from 'node:os'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DIST = path.resolve(__dirname, '../apps/mini-hq/dist'); const PORT = Number(process.env.HQ_PREVIEW_PORT || 5176); const API_TARGET = (process.env.VITE_API_TARGET || 'http://localhost:3000').replace(/\/$/, ''); const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', }; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** 释放预览端口(pnpm preview:hq 重复执行时自动重启) */ function freePort(port) { try { if (platform() === 'win32') { const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' }); const pids = new Set(); for (const line of out.split('\n')) { if (!line.includes('LISTENING')) continue; const pid = line.trim().split(/\s+/).pop(); if (pid && /^\d+$/.test(pid)) pids.add(pid); } for (const pid of pids) { try { execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' }); } catch { /* ignore */ } } return; } execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null || true`, { shell: true, stdio: 'ignore', }); } catch { /* 端口可能本就空闲 */ } } function sendFile(res, filePath) { const ext = path.extname(filePath); const type = MIME[ext] || 'application/octet-stream'; fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404); res.end('Not found'); return; } res.writeHead(200, { 'Content-Type': type }); res.end(data); }); } function proxyApi(req, res) { const target = new URL(req.url, API_TARGET); const headers = { ...req.headers, host: target.host }; const proxyReq = http.request( { hostname: target.hostname, port: target.port || (target.protocol === 'https:' ? 443 : 80), path: target.pathname + target.search, method: req.method, headers, }, (proxyRes) => { res.writeHead(proxyRes.statusCode || 502, proxyRes.headers); proxyRes.pipe(res); }, ); proxyReq.on('error', () => { res.writeHead(502, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 502, message: 'API 不可达,请先启动 pnpm dev:api' })); }); req.pipe(proxyReq); } function createServer() { return http.createServer((req, res) => { const urlPath = req.url?.split('?')[0] || '/'; if (urlPath.startsWith('/api')) { proxyApi(req, res); return; } let filePath = path.join(DIST, urlPath === '/' ? 'index.html' : urlPath); if (!filePath.startsWith(DIST)) { res.writeHead(403); res.end('Forbidden'); return; } fs.stat(filePath, (err, stat) => { if (!err && stat.isFile()) { sendFile(res, filePath); return; } sendFile(res, path.join(DIST, 'index.html')); }); }); } function listen(server, port) { return new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, () => { server.off('error', reject); resolve(); }); }); } async function main() { if (!fs.existsSync(DIST)) { console.error('未找到 apps/mini-hq/dist,请先执行:pnpm --filter @dukang/mini-hq build'); process.exit(1); } freePort(PORT); await sleep(400); const server = createServer(); try { await listen(server, PORT); } catch (err) { if (err && err.code === 'EADDRINUSE') { console.error(`端口 ${PORT} 仍被占用,请手动结束进程后重试,或设置 HQ_PREVIEW_PORT 换端口。`); process.exit(1); } throw err; } console.log(`mini-hq preview: http://localhost:${PORT}`); console.log(`API proxy: ${API_TARGET}`); } main().catch((err) => { console.error(err); process.exit(1); });