#!/usr/bin/env node /** * Nox(夜神模拟器)运行/测试助手 * * 背景:Nox 自带 adb(协议36) 与 HBuilderX 自带 adb(协议41) 会互相 kill server 造成设备掉线, * 因此本脚本统一使用 HBuilderX 的 adb,并通过 TCP `connect 127.0.0.1:62001` 挂载 Nox。 * * 两种在 Nox 上测试本项目的方式: * 1) test :构建/复用 H5 产物 -> 局域网静态服务 -> adb 唤起 Nox 浏览器打开(零打包,当前即可用) * 2) native:调用 HBuilderX CLI `launch app-android` 运行到安卓基座(需先在 HBuilderX 里以 uni-app 方式导入本项目) * * 用法: * node scripts/nox.mjs connect * node scripts/nox.mjs serve [--build] [--port 5199] * node scripts/nox.mjs open [--url http://ip:port] * node scripts/nox.mjs test [--build] [--port 5199] [--shot] * node scripts/nox.mjs screenshot * node scripts/nox.mjs native */ import { spawn, spawnSync } from "node:child_process"; import http from "node:http"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, ".."); // ---- 可配置项(均可用环境变量覆盖)---- const HBX_HOME = process.env.HBX_HOME || "C:\\apps\\HBuilderX"; const ADB = process.env.NOX_ADB || path.join(HBX_HOME, "plugins", "launcher-tools", "tools", "adbs", "adb.exe"); const CLI = process.env.HBX_CLI || path.join(HBX_HOME, "cli.exe"); const NOX_SERIAL = process.env.NOX_SERIAL || "127.0.0.1:62001"; const PROJECT_NAME = process.env.NOX_PROJECT || "security"; const H5_DIST = path.join(ROOT, "dist", "build", "h5"); const args = process.argv.slice(2); const cmd = args[0] || "test"; const getFlag = (name, def) => { const i = args.indexOf(`--${name}`); if (i === -1) return def; const next = args[i + 1]; return next && !next.startsWith("--") ? next : true; }; const hasFlag = (name) => args.includes(`--${name}`); function lanIp() { const ifaces = os.networkInterfaces(); for (const name of Object.keys(ifaces)) { for (const info of ifaces[name] || []) { if (info.family === "IPv4" && !info.internal) return info.address; } } return "127.0.0.1"; } function adb(...adbArgs) { return spawnSync(ADB, adbArgs, { encoding: "utf8" }); } function connect() { const r = adb("connect", NOX_SERIAL); console.log((r.stdout || "") + (r.stderr || "")); const list = adb("devices"); console.log(list.stdout || ""); if (!(list.stdout || "").includes(NOX_SERIAL)) { console.error(`[nox] 未发现设备 ${NOX_SERIAL},请确认夜神模拟器已启动。`); process.exit(1); } return true; } const MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".webp": "image/webp", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".map": "application/json; charset=utf-8", }; function serve(port) { if (!fs.existsSync(path.join(H5_DIST, "index.html"))) { console.error( `[nox] 未找到 H5 产物:${H5_DIST},请先执行 build:h5 或加 --build。`, ); process.exit(1); } const server = http.createServer((req, res) => { const urlPath = decodeURIComponent((req.url || "/").split("?")[0]); let filePath = path.join(H5_DIST, urlPath === "/" ? "index.html" : urlPath); if (!filePath.startsWith(H5_DIST)) { res.writeHead(403); return res.end("forbidden"); } if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) { filePath = path.join(filePath, "index.html"); } if (!fs.existsSync(filePath)) { // SPA 兜底:未知路径回 index.html filePath = path.join(H5_DIST, "index.html"); } res.writeHead(200, { "Content-Type": MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream", }); fs.createReadStream(filePath).pipe(res); }); server.listen(port, "0.0.0.0", () => { console.log( `[nox] 静态服务已启动:http://${lanIp()}:${port} (Ctrl+C 停止)`, ); }); return server; } function openBrowser(url) { const r = adb( "-s", NOX_SERIAL, "shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url, ); console.log((r.stdout || "") + (r.stderr || "")); } function screenshot() { const out = path.join(ROOT, "unpackage", "nox-screenshot.png"); fs.mkdirSync(path.dirname(out), { recursive: true }); const r = spawnSync(ADB, ["-s", NOX_SERIAL, "exec-out", "screencap", "-p"], { encoding: "buffer", maxBuffer: 64 * 1024 * 1024, }); if (r.error || !r.stdout || r.stdout.length === 0) { console.error( "[nox] 截图失败:设备可能离线,先执行 node scripts/nox.mjs connect 后重试", ); return; } fs.writeFileSync(out, r.stdout); console.log(`[nox] 截图已保存:${out}(${r.stdout.length} 字节)`); } function run(cmdName, cmdArgs) { const r = spawnSync(cmdName, cmdArgs, { encoding: "utf8", shell: false }); console.log((r.stdout || "") + (r.stderr || "")); return r; } function buildH5() { console.log("[nox] 构建 H5 产物 ..."); const npmCmd = process.platform === "win32" ? "npm" : "npm"; const r = spawnSync(npmCmd, ["run", "build:h5"], { cwd: ROOT, stdio: "inherit", shell: true, }); if (r.status !== 0) { console.error("[nox] 构建失败"); process.exit(1); } } function native() { console.log( "[nox] 通过 HBuilderX CLI 运行到安卓基座(需项目已在 HBuilderX 中识别为 uni-app)...", ); run(CLI, [ "launch", "app-android", "--project", PROJECT_NAME, "--deviceId", NOX_SERIAL, "--playground", "standard", ]); } const port = Number(getFlag("port", 5199)); switch (cmd) { case "connect": connect(); break; case "serve": if (hasFlag("build")) buildH5(); connect(); serve(port); break; case "open": connect(); openBrowser(getFlag("url", `http://${lanIp()}:${port}`)); break; case "screenshot": connect(); screenshot(); break; case "native": connect(); native(); break; case "test": default: { if (hasFlag("build")) buildH5(); connect(); const server = serve(port); const url = `http://${lanIp()}:${port}`; setTimeout(() => { openBrowser(url); console.log(`[nox] 已在 Nox 浏览器打开:${url}`); if (hasFlag("shot")) setTimeout(screenshot, 4000); }, 800); process.on("SIGINT", () => { server.close(); process.exit(0); }); break; } }