240 lines
6.8 KiB
JavaScript
240 lines
6.8 KiB
JavaScript
#!/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;
|
||
}
|
||
}
|