opticore-webapp 1.0.71 → 1.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -426,6 +426,244 @@ var getEnvFileLoadingService = (filePath, localLanguage) => {
426
426
  }
427
427
  };
428
428
 
429
+ // src/hotReload/hotReload.watcher.ts
430
+ import { watch as fsWatch } from "fs";
431
+ import { readdir, access } from "fs/promises";
432
+ import { join as join2, resolve as resolve2, extname, relative, basename as basename2 } from "path";
433
+ import { config as dotenvConfig } from "dotenv";
434
+ import chalk2 from "chalk";
435
+ import colors2 from "ansi-colors";
436
+ import gradient from "gradient-string";
437
+ var DEFAULT_WATCH_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs", ".json"];
438
+ var DEFAULT_HOT_RELOAD_EXTENSIONS = [".json"];
439
+ var DEFAULT_IGNORE = [
440
+ "node_modules",
441
+ "dist",
442
+ ".git",
443
+ "package.json",
444
+ "package-lock.json",
445
+ ".idea",
446
+ ".vscode",
447
+ "coverage",
448
+ "logs"
449
+ ];
450
+ var DEFAULT_DEBOUNCE_MS = 300;
451
+ var HotReloadWatcher = class {
452
+ cfg;
453
+ server = null;
454
+ watchers = [];
455
+ debounceTimer = null;
456
+ restarting = false;
457
+ constructor(config) {
458
+ const c = config ?? {};
459
+ this.cfg = {
460
+ rootDir: resolve2(c.rootDir ?? process.cwd()),
461
+ watchDirs: c.watchDirs ?? [],
462
+ watchExtensions: c.watchExtensions ?? DEFAULT_WATCH_EXTENSIONS,
463
+ ignore: [...DEFAULT_IGNORE, ...c.ignore ?? []],
464
+ envFile: c.envFile ?? ".env",
465
+ hotReloadExtensions: c.hotReloadExtensions ?? DEFAULT_HOT_RELOAD_EXTENSIONS,
466
+ debounceMs: c.debounceMs ?? DEFAULT_DEBOUNCE_MS
467
+ };
468
+ }
469
+ // ─── Called by WebServerCore once the HTTP server is listening ────────────
470
+ async attach(server) {
471
+ this.server = server;
472
+ this.printBanner();
473
+ await this.setupWatchers();
474
+ this.setupProcessSignals();
475
+ }
476
+ // ─── File watching ────────────────────────────────────────────────────────
477
+ async setupWatchers() {
478
+ const dirs = [
479
+ this.cfg.rootDir,
480
+ ...this.cfg.watchDirs.map((d) => resolve2(d))
481
+ ];
482
+ for (const dir of dirs) {
483
+ await this.watchRecursive(dir);
484
+ }
485
+ }
486
+ async watchRecursive(dir) {
487
+ if (this.shouldIgnoreDir(dir)) return;
488
+ try {
489
+ await access(dir);
490
+ } catch {
491
+ return;
492
+ }
493
+ try {
494
+ const w = fsWatch(dir, (_, filename) => {
495
+ if (filename) this.onFileChange(join2(dir, filename));
496
+ });
497
+ w.on("error", () => {
498
+ });
499
+ this.watchers.push(w);
500
+ const entries = await readdir(dir, { withFileTypes: true });
501
+ for (const entry of entries) {
502
+ if (entry.isDirectory()) await this.watchRecursive(join2(dir, entry.name));
503
+ }
504
+ } catch {
505
+ }
506
+ }
507
+ // ─── Change handling ──────────────────────────────────────────────────────
508
+ onFileChange(filePath) {
509
+ if (this.shouldIgnoreFile(filePath)) return;
510
+ if (!this.isWatched(filePath)) return;
511
+ if (this.restarting) return;
512
+ const rel = relative(this.cfg.rootDir, filePath);
513
+ if (this.isHotReloadable(filePath)) {
514
+ this.doHotReload(filePath, rel);
515
+ } else {
516
+ this.scheduleRestart(rel);
517
+ }
518
+ }
519
+ doHotReload(filePath, rel) {
520
+ if (this.isEnvFile(filePath)) {
521
+ dotenvConfig({ path: filePath, override: true });
522
+ this.printHot(rel, "env variables reloaded");
523
+ } else {
524
+ this.printHot(rel, "json config reloaded");
525
+ }
526
+ }
527
+ scheduleRestart(rel) {
528
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
529
+ this.debounceTimer = setTimeout(() => {
530
+ this.restarting = true;
531
+ this.printReloading(rel);
532
+ this.closeAndExit();
533
+ }, this.cfg.debounceMs);
534
+ }
535
+ /**
536
+ * Gracefully close the HTTP server so in-flight requests can finish,
537
+ * then exit with code 0 — the external runner restarts the process.
538
+ */
539
+ closeAndExit() {
540
+ if (!this.server) {
541
+ process.exit(0);
542
+ return;
543
+ }
544
+ this.server.close(() => {
545
+ process.exit(0);
546
+ });
547
+ setTimeout(() => {
548
+ process.exit(0);
549
+ }, 3e3).unref();
550
+ }
551
+ // ─── Filters ──────────────────────────────────────────────────────────────
552
+ shouldIgnoreDir(dir) {
553
+ return this.cfg.ignore.some(
554
+ (p) => dir.includes(`/${p}`) || dir.endsWith(`/${p}`) || dir.endsWith(p)
555
+ );
556
+ }
557
+ shouldIgnoreFile(filePath) {
558
+ const name = basename2(filePath);
559
+ const rel = relative(this.cfg.rootDir, filePath);
560
+ return this.cfg.ignore.some(
561
+ (p) => name === p || rel.includes(p) || filePath.includes(`/${p}/`)
562
+ );
563
+ }
564
+ isWatched(filePath) {
565
+ if (this.isEnvFile(filePath)) return true;
566
+ return this.cfg.watchExtensions.includes(extname(filePath));
567
+ }
568
+ isHotReloadable(filePath) {
569
+ if (this.isEnvFile(filePath)) return true;
570
+ return this.cfg.hotReloadExtensions.includes(extname(filePath));
571
+ }
572
+ isEnvFile(filePath) {
573
+ const name = basename2(filePath);
574
+ return name === this.cfg.envFile || name.startsWith(".env");
575
+ }
576
+ // ─── ANSI strip (box width calculation) ───────────────────────────────────
577
+ strip(str) {
578
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
579
+ }
580
+ // ─── Timestamp ────────────────────────────────────────────────────────────
581
+ ts() {
582
+ const n = /* @__PURE__ */ new Date();
583
+ return [n.getHours(), n.getMinutes(), n.getSeconds()].map((v) => String(v).padStart(2, "0")).join(":");
584
+ }
585
+ // ─── Console output ───────────────────────────────────────────────────────
586
+ /**
587
+ * Startup banner — same bgGreen box style as CoreService.infoServer().
588
+ *
589
+ * OPTICORE HOT RELOAD ← gradient
590
+ * ████████████████████████████████ ← bgGreen border
591
+ * root ./src
592
+ * watching .ts .js .json .env
593
+ * debounce 300ms
594
+ * ████████████████████████████████ ← bgGreen border
595
+ * watching for changes...
596
+ */
597
+ printBanner() {
598
+ const rootRel = relative(process.cwd(), this.cfg.rootDir) || ".";
599
+ const extStr = [...this.cfg.watchExtensions, ".env"].join(" ");
600
+ const ignoreParts = this.cfg.ignore.slice(0, 4);
601
+ const ignoreMore = this.cfg.ignore.length > 4 ? ` ${chalk2.blackBright(`+${this.cfg.ignore.length - 4} more`)}` : "";
602
+ const ignoreStr = chalk2.blackBright(ignoreParts.join(" ")) + ignoreMore;
603
+ const TITLE = "OPTICORE HOT RELOAD";
604
+ const rows = [
605
+ ` root ${chalk2.white(rootRel)}`,
606
+ ` watching ${chalk2.cyan(extStr)}`,
607
+ ` ignoring ${ignoreStr}`,
608
+ ` debounce ${chalk2.white(`${this.cfg.debounceMs}ms`)}`
609
+ ];
610
+ const maxLen = Math.max(
611
+ TITLE.length + 4,
612
+ ...rows.map((r) => this.strip(r).length)
613
+ ) + 4;
614
+ const border = chalk2.bgGreen.white(" ".repeat(maxLen));
615
+ const titlePad = " ".repeat(Math.max(0, Math.floor((maxLen - TITLE.length) / 2)));
616
+ console.log("\n" + titlePad + gradient(["#43e97b", "#38f9d7", "#00c6fb"])(TITLE));
617
+ console.log(border);
618
+ for (const row of rows) {
619
+ const pad = Math.max(0, maxLen - this.strip(row).length - 2);
620
+ console.log(chalk2.bgGreen.white(` ${row}${" ".repeat(pad)} `));
621
+ }
622
+ console.log(border);
623
+ console.log(chalk2.blackBright(` watching for changes...
624
+ `));
625
+ }
626
+ /**
627
+ * ✔ [ HOT ] 14:23:45 | .env → env variables reloaded
628
+ */
629
+ printHot(file, action) {
630
+ const badge = colors2.bgCyan(colors2.white.bold(" HOT "));
631
+ const filePart = chalk2.cyan.bold(file.padEnd(32));
632
+ console.log(
633
+ ` ${chalk2.green("\u2714")} ${badge} ${chalk2.blackBright(this.ts())} ${chalk2.blackBright("|")} ${filePart} ${chalk2.green("\u2192")} ${chalk2.green(action)}`
634
+ );
635
+ }
636
+ /**
637
+ * ⚡ [ RELOAD ] 14:24:03 | src/routes/user.ts → restarting server...
638
+ */
639
+ printReloading(file) {
640
+ const badge = colors2.bgYellow(colors2.white.bold(" RELOAD "));
641
+ const filePart = chalk2.yellow.bold(file.padEnd(32));
642
+ console.log(
643
+ `
644
+ ${chalk2.yellow("\u26A1")} ${badge} ${chalk2.blackBright(this.ts())} ${chalk2.blackBright("|")} ${filePart} ${chalk2.yellow("\u2192")} ${chalk2.yellow("restarting server...")}
645
+ `
646
+ );
647
+ }
648
+ // ─── Graceful shutdown on CTRL+C ──────────────────────────────────────────
649
+ setupProcessSignals() {
650
+ const shutdown = () => {
651
+ const badge = colors2.bgBlackBright(colors2.white.bold(" STOPPED"));
652
+ console.log(
653
+ `
654
+ ${chalk2.gray("\u25A0")} ${badge} ${chalk2.blackBright(this.ts())} ${chalk2.blackBright("|")} ${chalk2.gray("watcher stopped")}
655
+ `
656
+ );
657
+ this.watchers.forEach((w) => {
658
+ w.close();
659
+ });
660
+ process.exit(0);
661
+ };
662
+ process.once("SIGINT", shutdown);
663
+ process.once("SIGTERM", shutdown);
664
+ }
665
+ };
666
+
429
667
  // src/core/webServer.core.ts
430
668
  var WebServerCore = class {
431
669
  serverUtility;
@@ -438,6 +676,7 @@ var WebServerCore = class {
438
676
  environmentPath;
439
677
  serverListenEvent;
440
678
  dependenciesRegistered = false;
679
+ hotReloadCfg;
441
680
  constructor(paramsConstructor) {
442
681
  this.loadTranslationFiles();
443
682
  this.stackTraceErrorHandling();
@@ -454,6 +693,8 @@ var WebServerCore = class {
454
693
  this.expressApp.use(corsOrigin(paramsConstructor.corsOriginOptions));
455
694
  this.serverListenEvent = new ServerListenEventError2(paramsConstructor.localLanguage);
456
695
  this.serverUtility = new CoreService(paramsConstructor.localLanguage, paramsConstructor.environmentPath);
696
+ const hr = paramsConstructor.hotReload;
697
+ this.hotReloadCfg = !hr ? null : hr === true ? {} : hr;
457
698
  }
458
699
  /**
459
700
  *
@@ -502,7 +743,7 @@ var WebServerCore = class {
502
743
  httpCodeValue: HttpStatusCode3.NOT_FOUND
503
744
  });
504
745
  } else {
505
- return this.expressApp.listen(
746
+ const server = this.expressApp.listen(
506
747
  Number(this.getEnvironmentValue.appPort),
507
748
  () => {
508
749
  this.loadTranslationFiles();
@@ -514,11 +755,16 @@ var WebServerCore = class {
514
755
  this.container.loadServices();
515
756
  this.expressApp.use(express2.static(path3.join(process6.cwd(), "public/template")));
516
757
  this.registerRoutes(routers);
758
+ const resolvedHmr = this.resolveHotReloadConfig();
759
+ if (resolvedHmr !== null) {
760
+ new HotReloadWatcher(resolvedHmr).attach(server);
761
+ }
517
762
  } catch (err) {
518
763
  SServerStartError(err, this.environmentPath);
519
764
  }
520
765
  }
521
766
  );
767
+ return server;
522
768
  }
523
769
  }
524
770
  /**
@@ -562,6 +808,62 @@ var WebServerCore = class {
562
808
  loadTranslationFiles() {
563
809
  loaderTranslationFile(this.localLanguage);
564
810
  }
811
+ /**
812
+ * Resolve the final HotReloadConfig by merging constructor config with
813
+ * HMR environment variables.
814
+ *
815
+ * Priority (highest → lowest):
816
+ * 1. Constructor hotReload properties (explicit code-level config)
817
+ * 2. HMR_* env variables (runtime / per-environment config)
818
+ * 3. HotReloadWatcher internal defaults (built-in fallbacks)
819
+ *
820
+ * The watcher starts when:
821
+ * - constructor passed hotReload: true | HotReloadConfig
822
+ * - OR HMR_ENABLED=true in the .env file
823
+ */
824
+ resolveHotReloadConfig() {
825
+ const env = this.getEnvironmentValue;
826
+ if (this.hotReloadCfg === null && !env.hmrEnabled) {
827
+ return null;
828
+ }
829
+ const base = this.hotReloadCfg ?? {};
830
+ const envExtensions = this.hmrExtractExtensions(env.hmrWatchPatterns);
831
+ const envIgnore = this.hmrExtractIgnore(env.hmrIgnorePatterns);
832
+ return {
833
+ rootDir: base.rootDir,
834
+ watchDirs: base.watchDirs,
835
+ watchExtensions: base.watchExtensions ?? (envExtensions.length ? envExtensions : void 0),
836
+ ignore: base.ignore ?? (envIgnore.length ? envIgnore : void 0),
837
+ hotReloadExtensions: base.hotReloadExtensions,
838
+ debounceMs: base.debounceMs ?? (env.hmrDebounceMs > 0 ? env.hmrDebounceMs : void 0),
839
+ envFile: base.envFile
840
+ };
841
+ }
842
+ /**
843
+ * Extract file extensions from glob patterns such as "src/** /*.ts".
844
+ * "src/** /*.ts"
845
+ * ".env" skipped, handled natively by the watcher
846
+ */
847
+ hmrExtractExtensions(patterns) {
848
+ const exts = /* @__PURE__ */ new Set();
849
+ for (const p of patterns) {
850
+ const m = p.match(/\*(\.[a-zA-Z0-9]+)$/);
851
+ if (m) {
852
+ exts.add(m[1]);
853
+ }
854
+ }
855
+ return [...exts];
856
+ }
857
+ /**
858
+ * Extract ignore names from glob patterns such as "node_modules/**".
859
+ * "node_modules/**" → "node_modules"
860
+ * "dist/**" → "dist"
861
+ */
862
+ hmrExtractIgnore(patterns) {
863
+ return [...new Set(
864
+ patterns.map((p) => p.split("/")[0]).filter(Boolean)
865
+ )];
866
+ }
565
867
  /**
566
868
  *
567
869
  * @param allFeatureRoutes
@@ -593,397 +895,6 @@ var WebServerCore = class {
593
895
  );
594
896
  }
595
897
  };
596
-
597
- // src/hotReload/hotReload.watcher.ts
598
- import { fork, spawn } from "child_process";
599
- import { watch as fsWatch } from "fs";
600
- import { readdir, access } from "fs/promises";
601
- import { join as join3, resolve as resolve2, extname, relative, basename as basename2 } from "path";
602
- import { config as dotenvConfig } from "dotenv";
603
- import chalk2 from "chalk";
604
- import colors2 from "ansi-colors";
605
- import gradient from "gradient-string";
606
- var DEFAULT_WATCH_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs", ".json"];
607
- var DEFAULT_HOT_RELOAD_EXTENSIONS = [".json"];
608
- var DEFAULT_IGNORE = [
609
- "node_modules",
610
- "dist",
611
- ".git",
612
- "package.json",
613
- "package-lock.json",
614
- ".idea",
615
- ".vscode",
616
- "coverage",
617
- "logs"
618
- ];
619
- var DEFAULT_DEBOUNCE_MS = 300;
620
- var DEFAULT_MAX_CRASH_RESTARTS = 5;
621
- var HotReloadWatcher = class {
622
- cfg;
623
- child = null;
624
- watchers = [];
625
- debounceTimer = null;
626
- isRestarting = false;
627
- crashRestartCount = 0;
628
- started = false;
629
- restartStart = 0;
630
- constructor(config) {
631
- this.cfg = {
632
- entry: resolve2(config.entry),
633
- runtime: config.runtime ?? "node",
634
- runtimeArgs: config.runtimeArgs ?? [],
635
- rootDir: resolve2(config.rootDir ?? process.cwd()),
636
- watchDirs: config.watchDirs ?? [],
637
- watchExtensions: config.watchExtensions ?? DEFAULT_WATCH_EXTENSIONS,
638
- ignore: [...DEFAULT_IGNORE, ...config.ignore ?? []],
639
- envFile: config.envFile ?? ".env",
640
- hotReloadExtensions: config.hotReloadExtensions ?? DEFAULT_HOT_RELOAD_EXTENSIONS,
641
- debounceMs: config.debounceMs ?? DEFAULT_DEBOUNCE_MS,
642
- restartOnCrash: config.restartOnCrash ?? true,
643
- maxCrashRestarts: config.maxCrashRestarts ?? DEFAULT_MAX_CRASH_RESTARTS
644
- };
645
- }
646
- // ─── Public API ──────────────────────────────────────────────────────────
647
- async start() {
648
- if (this.started) return;
649
- this.started = true;
650
- this.printBanner();
651
- this.spawnChild();
652
- await this.setupWatchers();
653
- this.setupProcessSignals();
654
- }
655
- async stop() {
656
- this.watchers.forEach((w) => {
657
- w.close();
658
- });
659
- this.watchers = [];
660
- await this.killChild(true);
661
- this.printStopped();
662
- }
663
- // ─── Child process management ────────────────────────────────────────────
664
- spawnChild() {
665
- const { entry, runtime, runtimeArgs } = this.cfg;
666
- try {
667
- this.child = runtime === "node" ? fork(entry, [], {
668
- stdio: ["inherit", "inherit", "inherit", "ipc"],
669
- env: process.env
670
- }) : spawn(runtime, [...runtimeArgs, entry], {
671
- stdio: "inherit",
672
- env: process.env,
673
- shell: false
674
- });
675
- this.child.on("exit", (code) => {
676
- if (this.isRestarting) return;
677
- if (code !== 0 && code !== null) {
678
- this.printCrash(code);
679
- if (this.cfg.restartOnCrash && this.crashRestartCount < this.cfg.maxCrashRestarts) {
680
- this.crashRestartCount++;
681
- this.printCrashRetry(this.crashRestartCount, this.cfg.maxCrashRestarts);
682
- setTimeout(() => {
683
- this.spawnChild();
684
- }, 1e3);
685
- } else if (this.crashRestartCount >= this.cfg.maxCrashRestarts) {
686
- this.printCrashLimit(this.cfg.maxCrashRestarts);
687
- process.exit(1);
688
- }
689
- }
690
- });
691
- this.child.on("error", (err) => {
692
- this.printError(`Failed to start child process: ${err.message}`);
693
- });
694
- } catch (err) {
695
- this.printError(`Cannot spawn '${runtime} ${entry}': ${err.message}`);
696
- process.exit(1);
697
- }
698
- }
699
- async killChild(silent = false) {
700
- if (!this.child) return;
701
- const child = this.child;
702
- this.child = null;
703
- await new Promise((done) => {
704
- const forceKill = setTimeout(() => {
705
- child.kill("SIGKILL");
706
- done();
707
- }, 3e3);
708
- child.once("exit", () => {
709
- clearTimeout(forceKill);
710
- done();
711
- });
712
- child.kill("SIGTERM");
713
- });
714
- }
715
- // ─── File watching ────────────────────────────────────────────────────────
716
- async setupWatchers() {
717
- const dirs = [
718
- this.cfg.rootDir,
719
- ...this.cfg.watchDirs.map((d) => resolve2(d))
720
- ];
721
- for (const dir of dirs) {
722
- await this.watchDirectoryRecursive(dir);
723
- }
724
- }
725
- async watchDirectoryRecursive(dir) {
726
- if (this.shouldIgnoreDir(dir)) return;
727
- try {
728
- await access(dir);
729
- } catch {
730
- return;
731
- }
732
- try {
733
- const watcher = fsWatch(dir, (event2, filename) => {
734
- if (!filename) return;
735
- this.onFileChange(join3(dir, filename));
736
- });
737
- watcher.on("error", () => {
738
- });
739
- this.watchers.push(watcher);
740
- const entries = await readdir(dir, { withFileTypes: true });
741
- for (const entry of entries) {
742
- if (entry.isDirectory()) {
743
- await this.watchDirectoryRecursive(join3(dir, entry.name));
744
- }
745
- }
746
- } catch {
747
- }
748
- }
749
- // ─── Change handling ─────────────────────────────────────────────────────
750
- onFileChange(filePath) {
751
- if (this.shouldIgnoreFile(filePath)) return;
752
- if (!this.isWatchedFile(filePath)) return;
753
- const rel = relative(this.cfg.rootDir, filePath);
754
- if (this.isHotReloadable(filePath)) {
755
- this.doHotReload(filePath, rel);
756
- } else {
757
- this.scheduleRestart(rel);
758
- }
759
- }
760
- doHotReload(filePath, relativePath) {
761
- if (this.isEnvFile(filePath)) {
762
- dotenvConfig({ path: filePath, override: true });
763
- this.printHot(relativePath, "env variables reloaded");
764
- this.sendIpc({ type: "hot-reload", file: relativePath, kind: "env" });
765
- } else {
766
- this.printHot(relativePath, "json config reloaded");
767
- this.sendIpc({ type: "hot-reload", file: relativePath, kind: "json" });
768
- }
769
- }
770
- scheduleRestart(relativePath) {
771
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
772
- this.debounceTimer = setTimeout(async () => {
773
- this.printReloading(relativePath);
774
- this.restartStart = Date.now();
775
- this.isRestarting = true;
776
- this.crashRestartCount = 0;
777
- await this.killChild(true);
778
- this.isRestarting = false;
779
- this.spawnChild();
780
- this.printReady(Date.now() - this.restartStart);
781
- }, this.cfg.debounceMs);
782
- }
783
- // ─── IPC ─────────────────────────────────────────────────────────────────
784
- sendIpc(message) {
785
- if (this.child && typeof this.child.send === "function") {
786
- try {
787
- this.child.send(message);
788
- } catch {
789
- }
790
- }
791
- }
792
- // ─── Ignore / watch filters ───────────────────────────────────────────────
793
- shouldIgnoreDir(dirPath) {
794
- return this.cfg.ignore.some((p) => dirPath.includes(`/${p}`) || dirPath.endsWith(p));
795
- }
796
- shouldIgnoreFile(filePath) {
797
- const name = basename2(filePath);
798
- const rel = relative(this.cfg.rootDir, filePath);
799
- return this.cfg.ignore.some((p) => name === p || rel.includes(p) || filePath.includes(`/${p}/`));
800
- }
801
- isWatchedFile(filePath) {
802
- if (this.isEnvFile(filePath)) return true;
803
- return this.cfg.watchExtensions.includes(extname(filePath));
804
- }
805
- isHotReloadable(filePath) {
806
- if (this.isEnvFile(filePath)) return true;
807
- return this.cfg.hotReloadExtensions.includes(extname(filePath));
808
- }
809
- isEnvFile(filePath) {
810
- const name = basename2(filePath);
811
- return name === this.cfg.envFile || name.startsWith(".env");
812
- }
813
- // ─── ANSI strip helper (for width calculation) ────────────────────────────
814
- strip(str) {
815
- return str.replace(/\[[0-9;]*m/g, "").replace(/\][^]*/g, "");
816
- }
817
- // ─── Timestamp ────────────────────────────────────────────────────────────
818
- ts() {
819
- const n = /* @__PURE__ */ new Date();
820
- const h = String(n.getHours()).padStart(2, "0");
821
- const m = String(n.getMinutes()).padStart(2, "0");
822
- const s = String(n.getSeconds()).padStart(2, "0");
823
- return `${h}:${m}:${s}`;
824
- }
825
- // ─── Console output ───────────────────────────────────────────────────────
826
- /**
827
- * Startup banner — mirrors the infoServer() box style from CoreService.
828
- *
829
- * ╔══════════════════════════════════════════╗
830
- * gradient title
831
- * ╔══ bgGreen box ════════════════════════╗
832
- * entry dist/index.js
833
- * runtime node (IPC enabled)
834
- * root ./src
835
- * watching .ts .js .json .env
836
- * debounce 300ms
837
- * ╚══════════════════════════════════════════╝
838
- */
839
- printBanner() {
840
- const entryRel = relative(process.cwd(), this.cfg.entry) || this.cfg.entry;
841
- const rootRel = relative(process.cwd(), this.cfg.rootDir) || ".";
842
- const runtimeStr = this.cfg.runtime === "node" ? `node ${chalk2.blackBright("(IPC enabled)")}` : this.cfg.runtime;
843
- const extStr = [...this.cfg.watchExtensions, ".env"].join(" ");
844
- const ignoreStr = this.cfg.ignore.slice(0, 4).join(" ") + (this.cfg.ignore.length > 4 ? ` ${chalk2.blackBright(`+${this.cfg.ignore.length - 4} more`)}` : "");
845
- const TITLE = "OPTICORE HOT RELOAD";
846
- const rows = [
847
- ` entry ${chalk2.white.bold(entryRel)}`,
848
- ` runtime ${runtimeStr}`,
849
- ` root ${chalk2.white(rootRel)}`,
850
- ` watching ${chalk2.cyan(extStr)}`,
851
- ` ignoring ${chalk2.blackBright(ignoreStr)}`,
852
- ` debounce ${chalk2.white(`${this.cfg.debounceMs}ms`)}`
853
- ];
854
- const maxLen = Math.max(
855
- TITLE.length + 4,
856
- ...rows.map((r) => this.strip(r).length)
857
- ) + 4;
858
- const border = chalk2.bgGreen.white(" ".repeat(maxLen));
859
- const titlePad = " ".repeat(Math.max(0, Math.floor((maxLen - TITLE.length) / 2)));
860
- console.log("\n" + titlePad + gradient(["#43e97b", "#38f9d7", "#00c6fb"])(TITLE));
861
- console.log(border);
862
- rows.forEach((row) => {
863
- const cleanLen = this.strip(row).length;
864
- const padding = Math.max(0, maxLen - cleanLen - 2);
865
- console.log(chalk2.bgGreen.white(` ${row}${" ".repeat(padding)} `));
866
- });
867
- console.log(border);
868
- console.log(chalk2.blackBright(` watching for changes...
869
- `));
870
- }
871
- /**
872
- * HOT event — in-process reload, no server restart.
873
- *
874
- * ✔ [ HOT ] 14:23:45 | .env → env variables reloaded
875
- */
876
- printHot(file, action) {
877
- const badge = colors2.bgCyan(colors2.white.bold(" HOT "));
878
- const ts = chalk2.blackBright(this.ts());
879
- const sep = chalk2.blackBright("|");
880
- const filePart = chalk2.cyan.bold(file.padEnd(32));
881
- const arrow = chalk2.green("\u2192");
882
- const msg = chalk2.green(action);
883
- console.log(` ${chalk2.green("\u2714")} ${badge} ${ts} ${sep} ${filePart} ${arrow} ${msg}`);
884
- }
885
- /**
886
- * RELOAD event — server restart triggered.
887
- *
888
- * ⚡ [ RELOAD ] 14:24:03 | src/routes/user.ts → restarting server...
889
- */
890
- printReloading(file) {
891
- const badge = colors2.bgYellow(colors2.white.bold(" RELOAD "));
892
- const ts = chalk2.blackBright(this.ts());
893
- const sep = chalk2.blackBright("|");
894
- const filePart = chalk2.yellow.bold(file.padEnd(32));
895
- const arrow = chalk2.yellow("\u2192");
896
- const msg = chalk2.yellow("restarting server...");
897
- console.log(`
898
- ${chalk2.yellow("\u26A1")} ${badge} ${ts} ${sep} ${filePart} ${arrow} ${msg}`);
899
- }
900
- /**
901
- * READY event — server successfully restarted.
902
- * Uses the same full-width bgGreen border as infoServer().
903
- *
904
- * ════════════════════════════════════════════
905
- * READY server restarted in 245ms
906
- * ════════════════════════════════════════════
907
- */
908
- printReady(elapsedMs) {
909
- const msg = ` server restarted in ${chalk2.white.bold(`${elapsedMs}ms`)}`;
910
- const cleanLen = this.strip(msg).length;
911
- const width = Math.max(52, cleanLen + 6);
912
- const border = chalk2.bgGreen.white(" ".repeat(width));
913
- const padding = Math.max(0, width - cleanLen - 2);
914
- const line = chalk2.bgGreen.white(` ${chalk2.bgGreen.white.bold(" READY ")} ${msg}${" ".repeat(padding)} `);
915
- console.log(` ${border}`);
916
- console.log(` ${line}`);
917
- console.log(` ${border}
918
- `);
919
- }
920
- /**
921
- * CRASH event — unexpected child process exit.
922
- *
923
- * ✘ [ CRASH ] 14:24:10 | server exited with code 1
924
- */
925
- printCrash(code) {
926
- const badge = colors2.bgRed(colors2.white.bold(" CRASH "));
927
- const ts = chalk2.blackBright(this.ts());
928
- const sep = chalk2.blackBright("|");
929
- const msg = chalk2.red(`server exited with code ${chalk2.bold(String(code))}`);
930
- console.log(`
931
- ${chalk2.red("\u2718")} ${badge} ${ts} ${sep} ${msg}`);
932
- }
933
- /**
934
- * RETRY event — auto-restart after crash.
935
- *
936
- * ↺ [ RETRY ] 14:24:11 | attempt 1 / 5
937
- */
938
- printCrashRetry(attempt, max) {
939
- const badge = colors2.bgMagenta(colors2.white.bold(" RETRY "));
940
- const ts = chalk2.blackBright(this.ts());
941
- const sep = chalk2.blackBright("|");
942
- const msg = chalk2.magenta(`auto-restart attempt ${chalk2.bold(`${attempt} / ${max}`)}`);
943
- console.log(` ${chalk2.magenta("\u21BA")} ${badge} ${ts} ${sep} ${msg}`);
944
- }
945
- /**
946
- * LIMIT reached — give up restarting.
947
- */
948
- printCrashLimit(max) {
949
- const badge = colors2.bgRed(colors2.white.bold(" ERROR "));
950
- const ts = chalk2.blackBright(this.ts());
951
- const sep = chalk2.blackBright("|");
952
- const msg = chalk2.red(`max crash restarts reached (${chalk2.bold(String(max))}), giving up`);
953
- console.log(`
954
- ${chalk2.red("\u2718")} ${badge} ${ts} ${sep} ${msg}
955
- `);
956
- }
957
- /**
958
- * Error — miscellaneous internal error.
959
- */
960
- printError(message) {
961
- const badge = colors2.bgRed(colors2.white.bold(" ERROR "));
962
- const ts = chalk2.blackBright(this.ts());
963
- const sep = chalk2.blackBright("|");
964
- console.error(` ${chalk2.red("\u2718")} ${badge} ${ts} ${sep} ${chalk2.red(message)}`);
965
- }
966
- /**
967
- * Stopped — watcher shut down.
968
- */
969
- printStopped() {
970
- const badge = colors2.bgBlackBright(colors2.white.bold(" STOPPED"));
971
- const ts = chalk2.blackBright(this.ts());
972
- const sep = chalk2.blackBright("|");
973
- console.log(` ${chalk2.gray("\u25A0")} ${badge} ${ts} ${sep} ${chalk2.gray("watcher stopped")}
974
- `);
975
- }
976
- // ─── Graceful shutdown ────────────────────────────────────────────────────
977
- setupProcessSignals() {
978
- const shutdown = async () => {
979
- console.log("");
980
- await this.stop();
981
- process.exit(0);
982
- };
983
- process.once("SIGINT", shutdown);
984
- process.once("SIGTERM", shutdown);
985
- }
986
- };
987
898
  export {
988
899
  HotReloadWatcher,
989
900
  WebServerCore as WebServer,