const esbuild = require("esbuild") const fs = require("fs") const http = require("http") const path = require("path") const config = require(process.cwd() + (process.argv?.length > 3 ? "/" + process.argv[3] : "/esbuild.config.js")) const { watch } = require("chokidar") function log(str, clear) { if (clear && process.stdout.cursorTo && process.stdout.clearScreenDown) { process.stdout.cursorTo(0, 0) process.stdout.clearScreenDown() } console.log("\x1b[36m%s\x1b[0m", str) } let buildResults let ctx function cleanChunks() { const outdir = config.options.outdir if (!outdir) return const chunksDir = path.join(outdir, "chunks") if (fs.existsSync(chunksDir)) { fs.rmSync(chunksDir, { recursive: true }) } } async function build(catchError) { if (config.writeBuildInfo) { config.writeBuildInfo() } cleanChunks() if (!ctx) ctx = await esbuild.context(config.options) log((buildResults ? "re" : "") + "building...") const timerStart = Date.now() try { buildResults = await ctx.rebuild() if (config.options.metafile) { fs.writeFileSync( (config.options.outfile ? path.dirname(config.options.outfile) : config.options.outdir) + "/meta.json", JSON.stringify(buildResults.metafile, null, 4) ) } } catch (e) { console.log(e) if (!catchError) throw e } const timerEnd = Date.now() log(`built in ${timerEnd - timerStart}ms.`) } let bs switch (process.argv?.length > 2 ? process.argv[2] : "build") { case "serve": console.log("\x1b[36m%s\x1b[0mserving...") esbuild.context(config.options).then(function (_ctx) { _ctx.serve(config.serve).catch((err) => { console.error(err) process.exit(1) }) }) break case "start": bs = require("browser-sync") bs.init(config.browserSync) case "watch": // config.options.incremental = true build(true).then(() => { if (bs) { bs.reload() } }) // Trigger tibi-server project reload for api/ changes const apiDir = path.resolve("api") + "/" let reloadInFlight = false let reloadQueued = false function reloadTibi() { if (!config.tibiReloadUrl || !config.adminToken) { log("tibi-server reload: missing URL or token (running in mock mode?)") return } if (reloadInFlight) { reloadQueued = true log("tibi-server reload: already in flight, queuing another...") return } reloadInFlight = true const parts = new URL(config.tibiReloadUrl) const opts = { hostname: parts.hostname, port: parts.port, path: parts.pathname, method: "POST", headers: { Authorization: "Bearer " + config.adminToken }, timeout: 5000, } let body = "" const finishReload = () => { reloadInFlight = false if (reloadQueued) { reloadQueued = false log("tibi-server reload: executing queued reload...") reloadTibi() } } const req = http.request(opts, (res) => { res.on("data", (chunk) => (body += chunk)) res.on("end", () => { if (res.statusCode === 200) { log("tibi-server reloaded ✓") } else { console.error( "\x1b[33mtibi-server reload: HTTP " + res.statusCode + " — " + body.trim() + "\x1b[0m" ) } finishReload() }) }) req.on("error", (e) => { console.error("\x1b[33mtibi-server reload failed: " + e.message + "\x1b[0m") finishReload() }) req.end() } const { path: watchPaths, ignored: watchIgnored, ...watchRest } = config.watch const watcher = watch(watchPaths, { ignored: watchIgnored, ...watchRest }) log("watching files...") const DEBOUNCE_MS = 200 let frontendTimer, apiTimer watcher.on("change", function (changedPath) { log(`${changedPath} changed`, true) if (changedPath.startsWith(apiDir)) { clearTimeout(apiTimer) apiTimer = setTimeout(() => { log("api/ change detected → reloading tibi-server...") reloadTibi() }, DEBOUNCE_MS) } else { clearTimeout(frontendTimer) frontendTimer = setTimeout(() => { build(true).then(() => { if (bs) { bs.reload() } }) }, DEBOUNCE_MS) } }) break default: if (config.writeBuildInfo) { config.writeBuildInfo() } cleanChunks() esbuild.build(config.options).then(function (buildResults) { if (config.options.metafile) { fs.writeFileSync( (config.options.outfile ? path.dirname(config.options.outfile) : config.options.outdir) + "/meta.json", JSON.stringify(buildResults.metafile, null, 4) ) } }) }