mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +08:00
157 lines
4.4 KiB
JavaScript
157 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Resilient wrapper for the demo dev command.
|
|
//
|
|
// The demo runs through the CLI because it needs development-environment
|
|
// credentials, but it must not call `pnpm -w run cli`: that route invokes Turbo
|
|
// builds, and several package builds start by removing dist/, racing with the
|
|
// package dev watchers that the root dev server is already running.
|
|
//
|
|
// Instead, run the CLI from TypeScript source and ask it to launch the dashboard
|
|
// through the dashboard package's RDE production command. The CLI still owns the
|
|
// development-environment env vars, so this stays close to the packaged path.
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { watch } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
|
|
const scriptDir = import.meta.dirname;
|
|
const demoRoot = resolve(scriptDir, "..");
|
|
const repoRoot = resolve(demoRoot, "../..");
|
|
|
|
const LOG_PREFIX = "[Hexclave dev-retry] ";
|
|
const RETRY_DEBOUNCE_MS = 2_000;
|
|
const RETRY_TIMEOUT_MS = 5_000;
|
|
const portPrefix = process.env.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX ?? "81";
|
|
|
|
let cliChild;
|
|
let shutdownTimer;
|
|
|
|
function log(message) {
|
|
console.error(`${LOG_PREFIX}${message}`);
|
|
}
|
|
|
|
function spawnFromRepo(command, args, options = {}) {
|
|
return spawn(command, args, {
|
|
cwd: repoRoot,
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
function runCliDev() {
|
|
return new Promise((resolvePromise, reject) => {
|
|
cliChild = spawnFromRepo("pnpm", [
|
|
"exec", "tsx", "packages/cli/src/index.ts",
|
|
"dev",
|
|
"--no-auto-update",
|
|
"--config-file=./hexclave.config.ts",
|
|
"--",
|
|
"pnpm", "--dir", "examples/demo", "run", "dev:inner",
|
|
], {
|
|
detached: process.platform !== "win32",
|
|
env: {
|
|
...process.env,
|
|
HEXCLAVE_CLI_DEV_DASHBOARD_COMMAND: "pnpm --dir apps/dashboard run dev:rde-production",
|
|
STACK_API_URL: `http://localhost:${portPrefix}02`,
|
|
STACK_DASHBOARD_URL: `http://localhost:${portPrefix}01`,
|
|
STACK_CLI_PUBLISHABLE_CLIENT_KEY: "this-publishable-client-key-is-for-local-development-only",
|
|
STACK_CLI_NO_AUTO_UPDATE: "1",
|
|
},
|
|
});
|
|
|
|
cliChild.on("close", (code, signal) => {
|
|
cliChild = undefined;
|
|
resolvePromise({ code: code ?? 1, signalled: signal != null });
|
|
});
|
|
cliChild.on("error", (err) => {
|
|
cliChild = undefined;
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
function waitForFileChanges() {
|
|
return new Promise((resolvePromise) => {
|
|
const watchDirs = [
|
|
join(repoRoot, "apps", "dashboard"),
|
|
join(repoRoot, "packages"),
|
|
];
|
|
const watchers = [];
|
|
let resolved = false;
|
|
|
|
const done = () => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
for (const w of watchers) {
|
|
try { w.close(); } catch { /* ignore */ }
|
|
}
|
|
resolvePromise();
|
|
};
|
|
|
|
for (const dir of watchDirs) {
|
|
try {
|
|
const w = watch(dir, { recursive: true }, done);
|
|
w.on("error", () => { /* ignore watch errors */ });
|
|
watchers.push(w);
|
|
} catch {
|
|
// directory might not exist yet
|
|
}
|
|
}
|
|
|
|
// Dashboard startup can complete without a source-file change after the CLI
|
|
// has already failed its first health check, so always keep a timed retry.
|
|
setTimeout(done, RETRY_TIMEOUT_MS);
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
while (true) {
|
|
const { code, signalled } = await runCliDev();
|
|
|
|
if (signalled || code === 0) {
|
|
stopChildren("SIGTERM");
|
|
process.exit(code);
|
|
}
|
|
|
|
log(`Dev command exited with code ${code}. Watching for file changes before retrying...`);
|
|
await waitForFileChanges();
|
|
log(`Retrying in ${RETRY_DEBOUNCE_MS / 1000}s...`);
|
|
await sleep(RETRY_DEBOUNCE_MS);
|
|
}
|
|
}
|
|
|
|
function stopChildren(signal) {
|
|
if (cliChild != null && !cliChild.killed) {
|
|
try {
|
|
if (cliChild.pid != null && process.platform !== "win32") {
|
|
process.kill(-cliChild.pid, signal);
|
|
} else {
|
|
cliChild.kill(signal);
|
|
}
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
}
|
|
}
|
|
|
|
process.on("SIGINT", () => {
|
|
stopChildren("SIGINT");
|
|
shutdownTimer ??= setTimeout(() => process.exit(130), 5_000);
|
|
shutdownTimer.unref();
|
|
});
|
|
process.on("SIGTERM", () => {
|
|
stopChildren("SIGTERM");
|
|
shutdownTimer ??= setTimeout(() => process.exit(143), 5_000);
|
|
shutdownTimer.unref();
|
|
});
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
stopChildren("SIGTERM");
|
|
process.exit(1);
|
|
});
|