feat: add macOS compatibility with optional Playwright

This commit is contained in:
luckyyzh
2026-08-03 03:23:35 +08:00
parent 690020f823
commit dd47c3628d
9 changed files with 190 additions and 43 deletions
+76 -4
View File
@@ -1,5 +1,6 @@
import { spawn, spawnSync } from "node:child_process";
import { platform } from "node:os";
import { isLocalPortListening } from "./storage-maintenance.mjs";
import { rootDir } from "./profile.mjs";
const DEV_PORT = 30141;
@@ -49,12 +50,83 @@ exit 0
if (result.status !== 0) process.exit(result.status ?? 1);
}
if (platform() !== "win32") {
console.error("npm run restart currently supports Windows only. Stop the existing dev process and run npm run dev.");
process.exit(1);
function readMacListenerPids() {
const result = spawnSync(
"lsof",
["-nP", "-t", `-iTCP:${DEV_PORT}`, "-sTCP:LISTEN"],
{ cwd: rootDir, encoding: "utf8" },
);
if (result.error) throw result.error;
const output = result.stdout ?? "";
if (result.status !== 0 && !output.trim()) return [];
return [...new Set(
output
.trim()
.split(/\s+/)
.map((value) => Number.parseInt(value, 10))
.filter((pid) => Number.isInteger(pid) && pid > 0),
)];
}
stopWindowsDevelopmentProcesses();
function readMacProcessCommand(pid) {
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
cwd: rootDir,
encoding: "utf8",
});
if (result.error) throw result.error;
return result.status === 0 ? (result.stdout ?? "").trim() : "";
}
function signalProcess(pid, signal) {
try {
process.kill(pid, signal);
} catch (error) {
if (error?.code !== "ESRCH") throw error;
}
}
async function waitForPortClosed(timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (await isLocalPortListening(DEV_PORT)) {
if (Date.now() >= deadline) return false;
await new Promise((resolve) => setTimeout(resolve, 100));
}
return true;
}
async function stopMacDevelopmentProcesses() {
const listenerPids = readMacListenerPids();
if (listenerPids.length === 0) return;
const projectPids = listenerPids.filter((pid) => readMacProcessCommand(pid).includes(rootDir));
const foreignPids = listenerPids.filter((pid) => !projectPids.includes(pid));
if (foreignPids.length > 0 || projectPids.length === 0) {
const pids = foreignPids.length > 0 ? foreignPids : listenerPids;
throw new Error(`Port ${DEV_PORT} is occupied by a process that does not belong to this project (PID ${pids.join(", ")}).`);
}
for (const pid of projectPids) signalProcess(pid, "SIGTERM");
if (await waitForPortClosed(3000)) return;
for (const pid of projectPids) signalProcess(pid, "SIGKILL");
if (!(await waitForPortClosed(1000))) {
throw new Error(`Port ${DEV_PORT} is still occupied by this project after termination (PID ${projectPids.join(", ")}).`);
}
}
try {
if (platform() === "win32") {
stopWindowsDevelopmentProcesses();
} else if (platform() === "darwin") {
await stopMacDevelopmentProcesses();
} else {
throw new Error("npm run restart currently supports Windows and macOS only. Stop the existing dev process and run npm run dev.");
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
const npmCliPath = process.env.npm_execpath;
if (!npmCliPath) {