import { execSync, spawn } from "node:child_process";
import fs, { type WatchEventType } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createSpinner, defineCommand, type ProgressBar, run } from "archons";
import chalk from "chalk";
export interface WatchConfig {
paths: { path: string; cmd: string }[];
dev: {
exec: string;
options: string[];
};
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const watchMap: Record<string, WatchConfig> = {
core: {
paths: [{ path: path.join(__dirname, "../src"), cmd: "pnpm build" }],
dev: {
exec: "pnpm",
options: ["run", "--silent", "preview", "--clearScreen", "false"],
},
},
docs: {
paths: [{ path: path.join(__dirname, "../src"), cmd: "pnpm build" }],
dev: {
exec: "pnpm",
options: ["run", "--silent", "--filter", "@matechat/react-docs", "dev"],
},
},
};
const ignored = [
"node_modules",
".git",
".github",
".vscode",
"dist",
"build",
"bin",
".md",
".d.ts",
"target",
".cjs",
"doc_build",
];
function isIgnored(filename: string, ignored: string[]) {
for (const pattern of ignored) {
if (filename.includes(pattern)) {
return true;
}
}
return false;
}
function tryBuild(cmd: string, msg: string, spinner: ProgressBar) {
try {
spinner.setMessage(`${msg}`);
execSync(cmd, { stdio: "inherit" });
} catch (e) {
console.log(e);
spinner.println("[MateChat] Build failed.");
}
}
function createArchonsSpinner() {
const spinner = createSpinner();
spinner.setTemplate("{prefix:.green} {spinner:.yellow} {msg}");
spinner.setPrefix("[MateChat]");
return spinner;
}
export interface WatchOptions {
coloredPrefix: string;
options: WatchConfig;
displayPaths: string;
ignoredPaths: string[];
}
function watch({
coloredPrefix,
options,
displayPaths,
ignoredPaths,
}: WatchOptions) {
let lastBuild = Date.now();
function createDevProcess() {
let devProcess = spawn(options.dev.exec, options.dev.options, {
stdio: "inherit",
shell: true,
});
return () => {
const spinner = createArchonsSpinner();
spinner.println(`${coloredPrefix} Restarting dev server...`);
while (!devProcess.kill("SIGINT"));
devProcess.on("close", () => {
devProcess = spawn(options.dev.exec, options.dev.options, {
stdio: "inherit",
shell: true,
});
devProcess.on("error", (err) => {
console.error(`${coloredPrefix} Error restarting dev process:`, err);
});
spinner.finishAndClear();
});
};
}
const restartServer = createDevProcess();
function rebuild(
command: string,
eventType: WatchEventType,
filename: string | null,
) {
if (Date.now() - lastBuild < 1000) {
return;
}
restartServer();
if (filename && !isIgnored(filename, ignoredPaths)) {
const spinner = createArchonsSpinner();
spinner.println(
`${coloredPrefix} File ${chalk.cyan(
filename,
)} was ${eventType}d, rebuilding...`,
);
spinner.enableSteadyTick(100);
if ([".ts", ".tsx", ".css"].some((ext) => filename.endsWith(ext))) {
tryBuild(command, "Rebuilding...", spinner);
}
console.clear();
spinner.println(`${coloredPrefix} Build complete.\n\n`);
spinner.println(
`${coloredPrefix} Watching on ${displayPaths} for changes...`,
);
spinner.finishAndClear();
lastBuild = Date.now();
}
}
for (const { cmd, path } of options.paths) {
fs.watch(path, { recursive: true }, rebuild.bind(null, cmd));
}
}
const main = defineCommand({
meta: {
name: "watch",
about: "Watch files and rebuild when changed",
styled: true,
},
options: {
project: {
type: "option",
default: "core",
short: "p",
help: "Project to watch",
},
},
callback: async (ctx) => {
const cwd = process.cwd();
const project = ctx.args.project as string;
if (!watchMap[project]) {
throw new Error(`Project ${project} not found`);
}
const displayPaths = watchMap[project].paths
.map((opts) => chalk.cyan(path.relative(cwd, opts.path)))
.join(", ");
const coloredPrefix = chalk.green("[MateChat]");
const spinner = createArchonsSpinner();
spinner.enableSteadyTick(100);
spinner.setMessage("Initial building...");
for (const { cmd } of watchMap[project].paths) {
tryBuild(cmd, "Building...", spinner);
}
spinner.finishAndClear();
console.clear();
console.log(`${coloredPrefix} Build complete.\n`);
console.log(`${coloredPrefix} Watching on ${displayPaths} for changes...`);
watch({
coloredPrefix,
options: watchMap[project],
displayPaths,
ignoredPaths: ignored,
});
},
});
run(main);