artcraft/frontend/scripts/unix-dev.mjs

150 lines
4.8 KiB
JavaScript

// Invoked by script/artcraft/unix_dev.sh. Adapted from ArtcraftX's launcher.
// Living under frontend lets Node resolve the local Vite installation.
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/desktop/artcraft");
const host = "127.0.0.1";
const startingPort = Number(process.env.ARTCRAFT_DEV_PORT ?? "5193");
let vite;
let rust;
let stopping = false;
async function main() {
if (!Number.isInteger(startingPort) || startingPort < 1024 || startingPort > 65535) {
throw new Error("ARTCRAFT_DEV_PORT must be an integer between 1024 and 65535.");
}
process.env.VITE_ENVIRONMENT_TYPE ??= "production";
// Match the app build's cwd, including Tailwind config/content resolution.
process.chdir(appDir);
vite = await createServer({
configFile: path.join(appDir, "vite.config.ts"),
clearScreen: false,
server: {
host,
port: startingPort,
// Vite retries occupied ports while binding the real server. There is no
// port probe/release/rebind race, and no existing process is terminated.
strictPort: false,
open: false,
hmr: true,
},
});
if (stopping) return;
await vite.listen();
const { port } = vite.httpServer.address();
const devUrl = `http://${host}:${port}`;
// Only initial startup may choose another port. On a Vite config restart,
// rebind this port or fail visibly so Tauri never follows a silent port hop.
for (const options of [vite.config.server, vite.config.inlineConfig.server]) {
options.port = port;
options.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[artcraft] Frontend + HMR: ${devUrl}`);
console.log("[artcraft] Rust/Tauri: native IPC; automatic rebuild and app restart enabled.");
console.log("[artcraft] Press Ctrl-C to stop both.\n");
rust = spawn("cargo", [
"tauri", "dev",
"--no-dev-server",
// Tauri watches the desktop crate and its workspace dependencies itself.
// SQLx's external metadata and migrations also affect the Rust build.
"--additional-watch-folders", path.join(rootDir, ".sqlx"),
"--additional-watch-folders", path.join(rootDir, "_database/sql/artcraft_migrations"),
"--config", configPath,
"--config", JSON.stringify(overrides),
], {
cwd: rootDir,
// All cargo/rustc/app descendants inherit this private Unix process group.
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(`[artcraft] Rust/Tauri exited (${signal ?? code}).`);
void stop(code ?? 1);
}
});
}
async function stop(exitCode) {
if (stopping) return;
stopping = true;
console.log("\n[artcraft] Stopping frontend and Rust/Tauri...");
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 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;
}
}
function fail(error) {
console.error(`[artcraft] ${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);
main().catch(fail);