artcraftx/frontend/scripts/unix-dev.mjs

148 lines
4.7 KiB
JavaScript

// Invoked by script/unix_dev.sh. Keep this under frontend so Node resolves the
// workspace's installed Vite, without requiring a global Nx or another install.
import { spawn } from "node:child_process";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { createServer } from "vite";
const rootDir = fileURLToPath(new URL("../../", import.meta.url));
const frontendDir = path.join(rootDir, "frontend");
const appDir = path.join(frontendDir, "apps/artcraft");
const rustDir = path.join(rootDir, "crates/artcraftx");
const host = "127.0.0.1";
const startingPort = Number(process.env.ARTCRAFTX_DEV_PORT ?? "5183");
let vite;
let rust;
let stopping = false;
// cargo-tauri, cargo, rustc, and the app inherit this dedicated Unix process
// group. Never find or kill processes by port/name: ArtCraft may be running too.
function signalRustGroup(signal) {
if (!rust?.pid) return false;
try {
process.kill(-rust.pid, signal);
return true;
} catch (error) {
if (error.code !== "ESRCH") throw error;
return false;
}
}
async function stop(exitCode) {
if (stopping) return;
stopping = true;
console.log("\n[artcraftx] Stopping frontend and Rust/Tauri...");
// Bound shutdown even when a compiler, webview, or Vite plugin gets stuck.
const deadline = setTimeout(() => {
signalRustGroup("SIGKILL");
process.exit(exitCode);
}, 5000);
signalRustGroup("SIGTERM");
await Promise.allSettled([
vite?.close(),
(async () => {
while (signalRustGroup(0)) await delay(50);
})(),
]);
clearTimeout(deadline);
process.exit(exitCode);
}
function fail(error) {
console.error(`[artcraftx] ${error.stack ?? error}`);
void stop(1);
}
process.on("SIGINT", () => void stop(130));
process.on("SIGTERM", () => void stop(143));
process.on("SIGHUP", () => void stop(129));
process.on("uncaughtException", fail);
process.on("unhandledRejection", fail);
async function main() {
if (!Number.isInteger(startingPort) || startingPort < 1024 || startingPort > 65535) {
throw new Error("ARTCRAFTX_DEV_PORT must be an integer between 1024 and 65535.");
}
process.env.VITE_ENVIRONMENT_TYPE ??= "production";
process.chdir(appDir);
vite = await createServer({
configFile: path.join(appDir, "vite.config.ts"),
clearScreen: false,
server: {
host,
port: startingPort,
// Vite retries EADDRINUSE on successive ports while actually binding the
// server. There is no probe/release/rebind window for a competing app.
strictPort: false,
open: false,
hmr: true,
},
});
if (stopping) return;
await vite.listen();
const { port } = vite.httpServer.address();
const devUrl = `http://${host}:${port}`;
// On Vite config edits, restart on this same port or fail visibly. Silently
// moving again would leave Tauri pointing at the old (possibly foreign) app.
for (const serverOptions of [vite.config.server, vite.config.inlineConfig.server]) {
serverOptions.port = port;
serverOptions.strictPort = true;
}
const configPath = path.join(rustDir, "tauri-dev-hot-reload.conf.json");
const config = JSON.parse(await readFile(configPath, "utf8"));
if (stopping) return;
const csp = config.app.security.csp;
const overrides = {
build: { devUrl, beforeDevCommand: "" },
app: {
security: {
devCsp: {
...csp,
"connect-src": [...csp["connect-src"], devUrl, `ws://${host}:${port}`],
},
},
},
};
console.log(`\n[artcraftx] Frontend + HMR: ${devUrl}`);
console.log("[artcraftx] Rust/Tauri: native IPC; automatic rebuild and app restart enabled.");
console.log("[artcraftx] Press Ctrl-C to stop both.\n");
rust = spawn("cargo", [
"tauri", "dev",
"--no-dev-server",
"--config", configPath,
"--config", JSON.stringify(overrides),
], {
cwd: rootDir,
// Start a separate process group, but keep all output in this terminal.
detached: true,
stdio: ["ignore", "inherit", "inherit"],
env: {
...process.env,
TAURI_FRONTEND_PATH: frontendDir,
TAURI_APP_PATH: rustDir,
SQLX_OFFLINE: process.env.SQLX_OFFLINE ?? "true",
RUSTFLAGS: process.env.RUSTFLAGS ?? "-Awarnings",
WEBKIT_DISABLE_DMABUF_RENDERER: process.env.WEBKIT_DISABLE_DMABUF_RENDERER ?? "1",
WEBKIT_DISABLE_COMPOSITING_MODE: process.env.WEBKIT_DISABLE_COMPOSITING_MODE ?? "1",
},
});
rust.on("error", fail);
rust.on("exit", (code, signal) => {
if (!stopping) {
console.log(`[artcraftx] Rust/Tauri exited (${signal ?? code}).`);
void stop(code ?? 1);
}
});
}
main().catch(fail);