209 lines
4.6 KiB
JavaScript
209 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
|
|
const __dirname = import.meta.dirname;
|
|
const root = resolve(__dirname, "..");
|
|
const sourceBinPath = resolve(root, "bin", "sf-from-source");
|
|
const ensureResourcesPath = resolve(
|
|
root,
|
|
"scripts",
|
|
"ensure-source-resources.cjs",
|
|
);
|
|
const daemonCliPath = resolve(root, "packages", "daemon", "src", "cli-dev.ts");
|
|
const resolveTsPath = resolve(
|
|
root,
|
|
"src",
|
|
"resources",
|
|
"extensions",
|
|
"sf",
|
|
"tests",
|
|
"resolve-ts.mjs",
|
|
);
|
|
const WATCH_INTERVAL_MS = Number(
|
|
process.env.SF_DEV_SERVER_WATCH_INTERVAL_MS ?? 2_000,
|
|
);
|
|
const RESTART_GRACE_MS = Number(
|
|
process.env.SF_DEV_SERVER_RESTART_GRACE_MS ?? 5_000,
|
|
);
|
|
|
|
const passthroughArgs = process.argv.slice(2);
|
|
const oneShot = passthroughArgs.some((arg) =>
|
|
["--help", "-h", "--status", "--install", "--uninstall"].includes(arg),
|
|
);
|
|
const watchEnabled =
|
|
process.env.SF_DEV_SERVER_WATCH !== "0" && !oneShot && WATCH_INTERVAL_MS > 0;
|
|
|
|
const watchedRoots = [
|
|
resolve(root, "packages", "daemon", "src"),
|
|
resolve(root, "packages", "daemon", "package.json"),
|
|
resolve(root, "scripts", "dev-server.js"),
|
|
resolve(root, "scripts", "copy-resources.cjs"),
|
|
resolve(root, "scripts", "ensure-source-resources.cjs"),
|
|
resolve(root, "package.json"),
|
|
];
|
|
|
|
function newestMtimeMs(path) {
|
|
let latest = 0;
|
|
const stack = [path];
|
|
const skip = new Set(["dist", "node_modules", ".git", ".sf"]);
|
|
|
|
while (stack.length > 0) {
|
|
const current = stack.pop();
|
|
if (!current || !existsSync(current)) continue;
|
|
|
|
let stat;
|
|
try {
|
|
stat = statSync(current);
|
|
} catch {
|
|
continue;
|
|
}
|
|
latest = Math.max(latest, stat.mtimeMs);
|
|
|
|
if (!stat.isDirectory()) continue;
|
|
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(current, { withFileTypes: true });
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
if (skip.has(entry.name)) continue;
|
|
stack.push(join(current, entry.name));
|
|
}
|
|
}
|
|
|
|
return latest;
|
|
}
|
|
|
|
function sourceEpoch() {
|
|
return Math.max(...watchedRoots.map(newestMtimeMs));
|
|
}
|
|
|
|
const resourceBuild = spawnSync(process.execPath, [ensureResourcesPath], {
|
|
cwd: root,
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
});
|
|
|
|
if (resourceBuild.status !== 0) {
|
|
process.exit(resourceBuild.status ?? 1);
|
|
}
|
|
|
|
let child;
|
|
let stopping = false;
|
|
let restarting = false;
|
|
let currentEpoch = sourceEpoch();
|
|
let restartTimer;
|
|
|
|
function childEnv() {
|
|
return {
|
|
...process.env,
|
|
SF_SOURCE_ROOT: process.env.SF_SOURCE_ROOT || root,
|
|
SF_RUNTIME_SOURCE_ROOT: process.env.SF_RUNTIME_SOURCE_ROOT || root,
|
|
SF_BIN_PATH: process.env.SF_BIN_PATH || resolve(root, "dist", "loader.js"),
|
|
SF_CLI_PATH: process.env.SF_CLI_PATH || sourceBinPath,
|
|
};
|
|
}
|
|
|
|
function spawnDaemon() {
|
|
child = spawn(
|
|
process.execPath,
|
|
[
|
|
"--import",
|
|
resolveTsPath,
|
|
"--experimental-strip-types",
|
|
"--no-warnings",
|
|
daemonCliPath,
|
|
...passthroughArgs,
|
|
],
|
|
{
|
|
cwd: process.cwd(),
|
|
stdio: "inherit",
|
|
env: childEnv(),
|
|
},
|
|
);
|
|
|
|
child.on("error", (error) => {
|
|
console.error(
|
|
`[forge] Failed to launch local dev server: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
if (!watchEnabled) process.exit(1);
|
|
});
|
|
|
|
child.on("exit", (code, signal) => {
|
|
child = undefined;
|
|
if (stopping) {
|
|
if (signal) {
|
|
process.kill(process.pid, signal);
|
|
return;
|
|
}
|
|
process.exit(code ?? 0);
|
|
}
|
|
if (restarting) {
|
|
restarting = false;
|
|
spawnDaemon();
|
|
return;
|
|
}
|
|
if (!watchEnabled) {
|
|
if (signal) {
|
|
process.kill(process.pid, signal);
|
|
return;
|
|
}
|
|
process.exit(code ?? 0);
|
|
}
|
|
|
|
console.error(
|
|
`[forge] sf-server exited (${signal ?? `code ${code ?? 0}`}); restarting in 2s...`,
|
|
);
|
|
setTimeout(spawnDaemon, 2_000);
|
|
});
|
|
}
|
|
|
|
function requestRestart(reason) {
|
|
if (stopping || restarting) return;
|
|
restarting = true;
|
|
console.error(`[forge] ${reason}; restarting sf-server dev child...`);
|
|
|
|
if (!child || child.killed) {
|
|
restarting = false;
|
|
spawnDaemon();
|
|
return;
|
|
}
|
|
|
|
const victim = child;
|
|
victim.kill("SIGTERM");
|
|
restartTimer = setTimeout(() => {
|
|
if (victim.exitCode == null && victim.signalCode == null) {
|
|
victim.kill("SIGKILL");
|
|
}
|
|
}, RESTART_GRACE_MS);
|
|
}
|
|
|
|
function stop(signal) {
|
|
stopping = true;
|
|
if (restartTimer) clearTimeout(restartTimer);
|
|
if (!child || child.killed) {
|
|
process.exit(0);
|
|
return;
|
|
}
|
|
child.kill(signal);
|
|
}
|
|
|
|
process.on("SIGINT", () => stop("SIGINT"));
|
|
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
|
|
spawnDaemon();
|
|
|
|
if (watchEnabled) {
|
|
setInterval(() => {
|
|
const nextEpoch = sourceEpoch();
|
|
if (nextEpoch <= currentEpoch) return;
|
|
currentEpoch = nextEpoch;
|
|
requestRestart("daemon/dev source changed");
|
|
}, WATCH_INTERVAL_MS);
|
|
}
|