@whimco/chisel-cli
Version:
156 lines (137 loc) • 4.17 kB
JavaScript
// lib/watch.js
const chokidar = require("chokidar");
const picomatch = require("picomatch");
const { spawn } = require("node:child_process");
const fs = require("fs");
const path = require("path");
const { emit } = require("./emitSentinel");
const { generateProjectJson } = require("./generateProject");
function rbxtscBin() {
const ext = process.platform === "win32" ? ".cmd" : "";
const local = path.join(process.cwd(), "node_modules", ".bin", "rbxtsc" + ext);
return fs.existsSync(local) ? `"${local}"` : "rbxtsc";
}
function loadConfig() {
const p = path.join(process.cwd(), "chisel.config.json");
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf8"));
return {
appsDir: "src/apps",
sentinelPath: "src/chisel/__generated__/manifest-hash.ts",
// compileCmd intentionally omitted; we derive a safe default below
};
}
function compileCmdFrom(cfg) {
const cmd = cfg && typeof cfg.compileCmd === "string" ? cfg.compileCmd.trim() : "";
return cmd.length > 0 ? cmd : `${rbxtscBin()} -p tsconfig.json`;
}
const cfg = loadConfig();
const COMPILE_CMD = compileCmdFrom(cfg);
const APPS_DIR = path.join(process.cwd(), cfg.appsDir);
const isManifestFile = picomatch(
["src/apps/*/server/**/*.ts", "src/apps/*/shared/**/*.ts", "src/global/shared/**/*.ts"],
{ dot: true }
);
function sh(cmd) {
return new Promise((res) => {
const p = spawn(cmd, { shell: true, stdio: "inherit" });
p.on("exit", (code) => res(code ?? 0));
});
}
function stamp(appsDir) {
const apps = fs.existsSync(appsDir)
? fs.readdirSync(appsDir).filter((n) => fs.statSync(path.join(appsDir, n)).isDirectory())
: [];
const stamp = [];
function walk(dir) {
if (!fs.existsSync(dir)) return;
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name);
const s = fs.statSync(p);
if (s.isDirectory()) walk(p);
else if (/\.(ts|tsx|d\.ts)$/.test(name)) {
stamp.push([path.relative(process.cwd(), p), s.mtimeMs]);
}
}
}
for (const app of apps) {
for (const sub of ["server", "shared"]) {
walk(path.join(appsDir, app, sub));
}
}
return { apps, stamp };
}
function writeIfChanged(file, content) {
if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === content) return false;
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
return true;
}
function emitClients(appsDir, apps) {
for (const app of apps) {
const out = path.join(appsDir, app, "client", "gen", "index.client.ts");
const content =
`// generated client stub for ${app}\n`;
writeIfChanged(out, content);
}
}
module.exports.run = () => {
let running = false;
let queued = false;
let timer = null;
let changedPath = null;
const debounce = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(pipeline, 80);
};
async function pipeline() {
if (running) {
queued = true;
return;
}
running = true;
try {
generateProjectJson();
const runCodegen = !changedPath || isManifestFile(changedPath);
if (runCodegen) {
const m = stamp(APPS_DIR);
emitClients(APPS_DIR, m.apps);
emit(m, cfg.sentinelPath);
}
const code = await sh(COMPILE_CMD);
if (code !== 0) console.error(`[rbxtsc] exited with code ${code}`);
} finally {
running = false;
if (queued) {
queued = false;
setTimeout(pipeline, 50);
}
}
}
function onEvent(fsPath) {
changedPath = fsPath ? fsPath.replace(/\\/g, "/") : null;
debounce();
}
chokidar
.watch(
[
"src/**/*.ts",
"src/**/*.tsx",
"!out/**",
"!lib/**",
"!dist/**",
"!node_modules/**",
],
{
ignoreInitial: false,
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },
}
)
.on("add", onEvent)
.on("change", onEvent)
.on("unlink", onEvent)
.on("ready", () => {
console.log("[chisel] watch ready — building once…");
changedPath = null;
pipeline().catch((e) => console.error(e));
});
};