opticore-webapp 1.0.71 → 1.0.73

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.cjs CHANGED
@@ -30,7 +30,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- HotReloadWatcher: () => HotReloadWatcher,
34
33
  WebServer: () => WebServerCore,
35
34
  envPath: () => envPath
36
35
  });
@@ -462,6 +461,7 @@ var getEnvFileLoadingService = (filePath, localLanguage) => {
462
461
  };
463
462
 
464
463
  // src/core/webServer.core.ts
464
+ var import_opticore_watcher = require("opticore-watcher");
465
465
  var WebServerCore = class {
466
466
  serverUtility;
467
467
  expressApp = (0, import_opticore_express2.express)();
@@ -473,6 +473,7 @@ var WebServerCore = class {
473
473
  environmentPath;
474
474
  serverListenEvent;
475
475
  dependenciesRegistered = false;
476
+ hotReloadCfg;
476
477
  constructor(paramsConstructor) {
477
478
  this.loadTranslationFiles();
478
479
  this.stackTraceErrorHandling();
@@ -489,6 +490,8 @@ var WebServerCore = class {
489
490
  this.expressApp.use((0, import_cors.default)(paramsConstructor.corsOriginOptions));
490
491
  this.serverListenEvent = new import_opticore_catch_exception_error3.ServerListenEventError(paramsConstructor.localLanguage);
491
492
  this.serverUtility = new CoreService(paramsConstructor.localLanguage, paramsConstructor.environmentPath);
493
+ const hr = paramsConstructor.hotReload;
494
+ this.hotReloadCfg = !hr ? null : hr === true ? {} : hr;
492
495
  }
493
496
  /**
494
497
  *
@@ -537,7 +540,7 @@ var WebServerCore = class {
537
540
  httpCodeValue: import_opticore_http_response4.HttpStatusCode.NOT_FOUND
538
541
  });
539
542
  } else {
540
- return this.expressApp.listen(
543
+ const server = this.expressApp.listen(
541
544
  Number(this.getEnvironmentValue.appPort),
542
545
  () => {
543
546
  this.loadTranslationFiles();
@@ -549,11 +552,16 @@ var WebServerCore = class {
549
552
  this.container.loadServices();
550
553
  this.expressApp.use(import_opticore_express2.express.static(path3.join(import_node_process4.default.cwd(), "public/template")));
551
554
  this.registerRoutes(routers);
555
+ const resolvedHmr = this.resolveHotReloadConfig();
556
+ if (resolvedHmr !== null) {
557
+ new import_opticore_watcher.HotReloadWatcher(resolvedHmr).attach(server);
558
+ }
552
559
  } catch (err) {
553
560
  SServerStartError(err, this.environmentPath);
554
561
  }
555
562
  }
556
563
  );
564
+ return server;
557
565
  }
558
566
  }
559
567
  /**
@@ -597,6 +605,62 @@ var WebServerCore = class {
597
605
  loadTranslationFiles() {
598
606
  loaderTranslationFile(this.localLanguage);
599
607
  }
608
+ /**
609
+ * Resolve the final HotReloadConfig by merging constructor config with
610
+ * HMR environment variables.
611
+ *
612
+ * Priority (highest → lowest):
613
+ * 1. Constructor hotReload properties (explicit code-level config)
614
+ * 2. HMR_* env variables (runtime / per-environment config)
615
+ * 3. HotReloadWatcher internal defaults (built-in fallbacks)
616
+ *
617
+ * The watcher starts when:
618
+ * - constructor passed hotReload: true | HotReloadConfig
619
+ * - OR HMR_ENABLED=true in the .env file
620
+ */
621
+ resolveHotReloadConfig() {
622
+ const env = this.getEnvironmentValue;
623
+ if (this.hotReloadCfg === null && !env.hmrEnabled) {
624
+ return null;
625
+ }
626
+ const base = this.hotReloadCfg ?? {};
627
+ const envExtensions = this.hmrExtractExtensions(env.hmrWatchPatterns);
628
+ const envIgnore = this.hmrExtractIgnore(env.hmrIgnorePatterns);
629
+ return {
630
+ rootDir: base.rootDir,
631
+ watchDirs: base.watchDirs,
632
+ watchExtensions: base.watchExtensions ?? (envExtensions.length ? envExtensions : void 0),
633
+ ignore: base.ignore ?? (envIgnore.length ? envIgnore : void 0),
634
+ hotReloadExtensions: base.hotReloadExtensions,
635
+ debounceMs: base.debounceMs ?? (env.hmrDebounceMs > 0 ? env.hmrDebounceMs : void 0),
636
+ envFile: base.envFile
637
+ };
638
+ }
639
+ /**
640
+ * Extract file extensions from glob patterns such as "src/** /*.ts".
641
+ * "src/** /*.ts"
642
+ * ".env" skipped, handled natively by the watcher
643
+ */
644
+ hmrExtractExtensions(patterns) {
645
+ const exts = /* @__PURE__ */ new Set();
646
+ for (const p of patterns) {
647
+ const m = p.match(/\*(\.[a-zA-Z0-9]+)$/);
648
+ if (m) {
649
+ exts.add(m[1]);
650
+ }
651
+ }
652
+ return [...exts];
653
+ }
654
+ /**
655
+ * Extract ignore names from glob patterns such as "node_modules/**".
656
+ * "node_modules/**" → "node_modules"
657
+ * "dist/**" → "dist"
658
+ */
659
+ hmrExtractIgnore(patterns) {
660
+ return [...new Set(
661
+ patterns.map((p) => p.split("/")[0]).filter(Boolean)
662
+ )];
663
+ }
600
664
  /**
601
665
  *
602
666
  * @param allFeatureRoutes
@@ -628,400 +692,8 @@ var WebServerCore = class {
628
692
  );
629
693
  }
630
694
  };
631
-
632
- // src/hotReload/hotReload.watcher.ts
633
- var import_child_process = require("child_process");
634
- var import_fs2 = require("fs");
635
- var import_promises = require("fs/promises");
636
- var import_path2 = require("path");
637
- var import_dotenv = require("dotenv");
638
- var import_chalk2 = __toESM(require("chalk"), 1);
639
- var import_ansi_colors2 = __toESM(require("ansi-colors"), 1);
640
- var import_gradient_string = __toESM(require("gradient-string"), 1);
641
- var DEFAULT_WATCH_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs", ".json"];
642
- var DEFAULT_HOT_RELOAD_EXTENSIONS = [".json"];
643
- var DEFAULT_IGNORE = [
644
- "node_modules",
645
- "dist",
646
- ".git",
647
- "package.json",
648
- "package-lock.json",
649
- ".idea",
650
- ".vscode",
651
- "coverage",
652
- "logs"
653
- ];
654
- var DEFAULT_DEBOUNCE_MS = 300;
655
- var DEFAULT_MAX_CRASH_RESTARTS = 5;
656
- var HotReloadWatcher = class {
657
- cfg;
658
- child = null;
659
- watchers = [];
660
- debounceTimer = null;
661
- isRestarting = false;
662
- crashRestartCount = 0;
663
- started = false;
664
- restartStart = 0;
665
- constructor(config) {
666
- this.cfg = {
667
- entry: (0, import_path2.resolve)(config.entry),
668
- runtime: config.runtime ?? "node",
669
- runtimeArgs: config.runtimeArgs ?? [],
670
- rootDir: (0, import_path2.resolve)(config.rootDir ?? process.cwd()),
671
- watchDirs: config.watchDirs ?? [],
672
- watchExtensions: config.watchExtensions ?? DEFAULT_WATCH_EXTENSIONS,
673
- ignore: [...DEFAULT_IGNORE, ...config.ignore ?? []],
674
- envFile: config.envFile ?? ".env",
675
- hotReloadExtensions: config.hotReloadExtensions ?? DEFAULT_HOT_RELOAD_EXTENSIONS,
676
- debounceMs: config.debounceMs ?? DEFAULT_DEBOUNCE_MS,
677
- restartOnCrash: config.restartOnCrash ?? true,
678
- maxCrashRestarts: config.maxCrashRestarts ?? DEFAULT_MAX_CRASH_RESTARTS
679
- };
680
- }
681
- // ─── Public API ──────────────────────────────────────────────────────────
682
- async start() {
683
- if (this.started) return;
684
- this.started = true;
685
- this.printBanner();
686
- this.spawnChild();
687
- await this.setupWatchers();
688
- this.setupProcessSignals();
689
- }
690
- async stop() {
691
- this.watchers.forEach((w) => {
692
- w.close();
693
- });
694
- this.watchers = [];
695
- await this.killChild(true);
696
- this.printStopped();
697
- }
698
- // ─── Child process management ────────────────────────────────────────────
699
- spawnChild() {
700
- const { entry, runtime, runtimeArgs } = this.cfg;
701
- try {
702
- this.child = runtime === "node" ? (0, import_child_process.fork)(entry, [], {
703
- stdio: ["inherit", "inherit", "inherit", "ipc"],
704
- env: process.env
705
- }) : (0, import_child_process.spawn)(runtime, [...runtimeArgs, entry], {
706
- stdio: "inherit",
707
- env: process.env,
708
- shell: false
709
- });
710
- this.child.on("exit", (code) => {
711
- if (this.isRestarting) return;
712
- if (code !== 0 && code !== null) {
713
- this.printCrash(code);
714
- if (this.cfg.restartOnCrash && this.crashRestartCount < this.cfg.maxCrashRestarts) {
715
- this.crashRestartCount++;
716
- this.printCrashRetry(this.crashRestartCount, this.cfg.maxCrashRestarts);
717
- setTimeout(() => {
718
- this.spawnChild();
719
- }, 1e3);
720
- } else if (this.crashRestartCount >= this.cfg.maxCrashRestarts) {
721
- this.printCrashLimit(this.cfg.maxCrashRestarts);
722
- process.exit(1);
723
- }
724
- }
725
- });
726
- this.child.on("error", (err) => {
727
- this.printError(`Failed to start child process: ${err.message}`);
728
- });
729
- } catch (err) {
730
- this.printError(`Cannot spawn '${runtime} ${entry}': ${err.message}`);
731
- process.exit(1);
732
- }
733
- }
734
- async killChild(silent = false) {
735
- if (!this.child) return;
736
- const child = this.child;
737
- this.child = null;
738
- await new Promise((done) => {
739
- const forceKill = setTimeout(() => {
740
- child.kill("SIGKILL");
741
- done();
742
- }, 3e3);
743
- child.once("exit", () => {
744
- clearTimeout(forceKill);
745
- done();
746
- });
747
- child.kill("SIGTERM");
748
- });
749
- }
750
- // ─── File watching ────────────────────────────────────────────────────────
751
- async setupWatchers() {
752
- const dirs = [
753
- this.cfg.rootDir,
754
- ...this.cfg.watchDirs.map((d) => (0, import_path2.resolve)(d))
755
- ];
756
- for (const dir of dirs) {
757
- await this.watchDirectoryRecursive(dir);
758
- }
759
- }
760
- async watchDirectoryRecursive(dir) {
761
- if (this.shouldIgnoreDir(dir)) return;
762
- try {
763
- await (0, import_promises.access)(dir);
764
- } catch {
765
- return;
766
- }
767
- try {
768
- const watcher = (0, import_fs2.watch)(dir, (event2, filename) => {
769
- if (!filename) return;
770
- this.onFileChange((0, import_path2.join)(dir, filename));
771
- });
772
- watcher.on("error", () => {
773
- });
774
- this.watchers.push(watcher);
775
- const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
776
- for (const entry of entries) {
777
- if (entry.isDirectory()) {
778
- await this.watchDirectoryRecursive((0, import_path2.join)(dir, entry.name));
779
- }
780
- }
781
- } catch {
782
- }
783
- }
784
- // ─── Change handling ─────────────────────────────────────────────────────
785
- onFileChange(filePath) {
786
- if (this.shouldIgnoreFile(filePath)) return;
787
- if (!this.isWatchedFile(filePath)) return;
788
- const rel = (0, import_path2.relative)(this.cfg.rootDir, filePath);
789
- if (this.isHotReloadable(filePath)) {
790
- this.doHotReload(filePath, rel);
791
- } else {
792
- this.scheduleRestart(rel);
793
- }
794
- }
795
- doHotReload(filePath, relativePath) {
796
- if (this.isEnvFile(filePath)) {
797
- (0, import_dotenv.config)({ path: filePath, override: true });
798
- this.printHot(relativePath, "env variables reloaded");
799
- this.sendIpc({ type: "hot-reload", file: relativePath, kind: "env" });
800
- } else {
801
- this.printHot(relativePath, "json config reloaded");
802
- this.sendIpc({ type: "hot-reload", file: relativePath, kind: "json" });
803
- }
804
- }
805
- scheduleRestart(relativePath) {
806
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
807
- this.debounceTimer = setTimeout(async () => {
808
- this.printReloading(relativePath);
809
- this.restartStart = Date.now();
810
- this.isRestarting = true;
811
- this.crashRestartCount = 0;
812
- await this.killChild(true);
813
- this.isRestarting = false;
814
- this.spawnChild();
815
- this.printReady(Date.now() - this.restartStart);
816
- }, this.cfg.debounceMs);
817
- }
818
- // ─── IPC ─────────────────────────────────────────────────────────────────
819
- sendIpc(message) {
820
- if (this.child && typeof this.child.send === "function") {
821
- try {
822
- this.child.send(message);
823
- } catch {
824
- }
825
- }
826
- }
827
- // ─── Ignore / watch filters ───────────────────────────────────────────────
828
- shouldIgnoreDir(dirPath) {
829
- return this.cfg.ignore.some((p) => dirPath.includes(`/${p}`) || dirPath.endsWith(p));
830
- }
831
- shouldIgnoreFile(filePath) {
832
- const name = (0, import_path2.basename)(filePath);
833
- const rel = (0, import_path2.relative)(this.cfg.rootDir, filePath);
834
- return this.cfg.ignore.some((p) => name === p || rel.includes(p) || filePath.includes(`/${p}/`));
835
- }
836
- isWatchedFile(filePath) {
837
- if (this.isEnvFile(filePath)) return true;
838
- return this.cfg.watchExtensions.includes((0, import_path2.extname)(filePath));
839
- }
840
- isHotReloadable(filePath) {
841
- if (this.isEnvFile(filePath)) return true;
842
- return this.cfg.hotReloadExtensions.includes((0, import_path2.extname)(filePath));
843
- }
844
- isEnvFile(filePath) {
845
- const name = (0, import_path2.basename)(filePath);
846
- return name === this.cfg.envFile || name.startsWith(".env");
847
- }
848
- // ─── ANSI strip helper (for width calculation) ────────────────────────────
849
- strip(str) {
850
- return str.replace(/\[[0-9;]*m/g, "").replace(/\][^]*/g, "");
851
- }
852
- // ─── Timestamp ────────────────────────────────────────────────────────────
853
- ts() {
854
- const n = /* @__PURE__ */ new Date();
855
- const h = String(n.getHours()).padStart(2, "0");
856
- const m = String(n.getMinutes()).padStart(2, "0");
857
- const s = String(n.getSeconds()).padStart(2, "0");
858
- return `${h}:${m}:${s}`;
859
- }
860
- // ─── Console output ───────────────────────────────────────────────────────
861
- /**
862
- * Startup banner — mirrors the infoServer() box style from CoreService.
863
- *
864
- * ╔══════════════════════════════════════════╗
865
- * gradient title
866
- * ╔══ bgGreen box ════════════════════════╗
867
- * entry dist/index.js
868
- * runtime node (IPC enabled)
869
- * root ./src
870
- * watching .ts .js .json .env
871
- * debounce 300ms
872
- * ╚══════════════════════════════════════════╝
873
- */
874
- printBanner() {
875
- const entryRel = (0, import_path2.relative)(process.cwd(), this.cfg.entry) || this.cfg.entry;
876
- const rootRel = (0, import_path2.relative)(process.cwd(), this.cfg.rootDir) || ".";
877
- const runtimeStr = this.cfg.runtime === "node" ? `node ${import_chalk2.default.blackBright("(IPC enabled)")}` : this.cfg.runtime;
878
- const extStr = [...this.cfg.watchExtensions, ".env"].join(" ");
879
- const ignoreStr = this.cfg.ignore.slice(0, 4).join(" ") + (this.cfg.ignore.length > 4 ? ` ${import_chalk2.default.blackBright(`+${this.cfg.ignore.length - 4} more`)}` : "");
880
- const TITLE = "OPTICORE HOT RELOAD";
881
- const rows = [
882
- ` entry ${import_chalk2.default.white.bold(entryRel)}`,
883
- ` runtime ${runtimeStr}`,
884
- ` root ${import_chalk2.default.white(rootRel)}`,
885
- ` watching ${import_chalk2.default.cyan(extStr)}`,
886
- ` ignoring ${import_chalk2.default.blackBright(ignoreStr)}`,
887
- ` debounce ${import_chalk2.default.white(`${this.cfg.debounceMs}ms`)}`
888
- ];
889
- const maxLen = Math.max(
890
- TITLE.length + 4,
891
- ...rows.map((r) => this.strip(r).length)
892
- ) + 4;
893
- const border = import_chalk2.default.bgGreen.white(" ".repeat(maxLen));
894
- const titlePad = " ".repeat(Math.max(0, Math.floor((maxLen - TITLE.length) / 2)));
895
- console.log("\n" + titlePad + (0, import_gradient_string.default)(["#43e97b", "#38f9d7", "#00c6fb"])(TITLE));
896
- console.log(border);
897
- rows.forEach((row) => {
898
- const cleanLen = this.strip(row).length;
899
- const padding = Math.max(0, maxLen - cleanLen - 2);
900
- console.log(import_chalk2.default.bgGreen.white(` ${row}${" ".repeat(padding)} `));
901
- });
902
- console.log(border);
903
- console.log(import_chalk2.default.blackBright(` watching for changes...
904
- `));
905
- }
906
- /**
907
- * HOT event — in-process reload, no server restart.
908
- *
909
- * ✔ [ HOT ] 14:23:45 | .env → env variables reloaded
910
- */
911
- printHot(file, action) {
912
- const badge = import_ansi_colors2.default.bgCyan(import_ansi_colors2.default.white.bold(" HOT "));
913
- const ts = import_chalk2.default.blackBright(this.ts());
914
- const sep = import_chalk2.default.blackBright("|");
915
- const filePart = import_chalk2.default.cyan.bold(file.padEnd(32));
916
- const arrow = import_chalk2.default.green("\u2192");
917
- const msg = import_chalk2.default.green(action);
918
- console.log(` ${import_chalk2.default.green("\u2714")} ${badge} ${ts} ${sep} ${filePart} ${arrow} ${msg}`);
919
- }
920
- /**
921
- * RELOAD event — server restart triggered.
922
- *
923
- * ⚡ [ RELOAD ] 14:24:03 | src/routes/user.ts → restarting server...
924
- */
925
- printReloading(file) {
926
- const badge = import_ansi_colors2.default.bgYellow(import_ansi_colors2.default.white.bold(" RELOAD "));
927
- const ts = import_chalk2.default.blackBright(this.ts());
928
- const sep = import_chalk2.default.blackBright("|");
929
- const filePart = import_chalk2.default.yellow.bold(file.padEnd(32));
930
- const arrow = import_chalk2.default.yellow("\u2192");
931
- const msg = import_chalk2.default.yellow("restarting server...");
932
- console.log(`
933
- ${import_chalk2.default.yellow("\u26A1")} ${badge} ${ts} ${sep} ${filePart} ${arrow} ${msg}`);
934
- }
935
- /**
936
- * READY event — server successfully restarted.
937
- * Uses the same full-width bgGreen border as infoServer().
938
- *
939
- * ════════════════════════════════════════════
940
- * READY server restarted in 245ms
941
- * ════════════════════════════════════════════
942
- */
943
- printReady(elapsedMs) {
944
- const msg = ` server restarted in ${import_chalk2.default.white.bold(`${elapsedMs}ms`)}`;
945
- const cleanLen = this.strip(msg).length;
946
- const width = Math.max(52, cleanLen + 6);
947
- const border = import_chalk2.default.bgGreen.white(" ".repeat(width));
948
- const padding = Math.max(0, width - cleanLen - 2);
949
- const line = import_chalk2.default.bgGreen.white(` ${import_chalk2.default.bgGreen.white.bold(" READY ")} ${msg}${" ".repeat(padding)} `);
950
- console.log(` ${border}`);
951
- console.log(` ${line}`);
952
- console.log(` ${border}
953
- `);
954
- }
955
- /**
956
- * CRASH event — unexpected child process exit.
957
- *
958
- * ✘ [ CRASH ] 14:24:10 | server exited with code 1
959
- */
960
- printCrash(code) {
961
- const badge = import_ansi_colors2.default.bgRed(import_ansi_colors2.default.white.bold(" CRASH "));
962
- const ts = import_chalk2.default.blackBright(this.ts());
963
- const sep = import_chalk2.default.blackBright("|");
964
- const msg = import_chalk2.default.red(`server exited with code ${import_chalk2.default.bold(String(code))}`);
965
- console.log(`
966
- ${import_chalk2.default.red("\u2718")} ${badge} ${ts} ${sep} ${msg}`);
967
- }
968
- /**
969
- * RETRY event — auto-restart after crash.
970
- *
971
- * ↺ [ RETRY ] 14:24:11 | attempt 1 / 5
972
- */
973
- printCrashRetry(attempt, max) {
974
- const badge = import_ansi_colors2.default.bgMagenta(import_ansi_colors2.default.white.bold(" RETRY "));
975
- const ts = import_chalk2.default.blackBright(this.ts());
976
- const sep = import_chalk2.default.blackBright("|");
977
- const msg = import_chalk2.default.magenta(`auto-restart attempt ${import_chalk2.default.bold(`${attempt} / ${max}`)}`);
978
- console.log(` ${import_chalk2.default.magenta("\u21BA")} ${badge} ${ts} ${sep} ${msg}`);
979
- }
980
- /**
981
- * LIMIT reached — give up restarting.
982
- */
983
- printCrashLimit(max) {
984
- const badge = import_ansi_colors2.default.bgRed(import_ansi_colors2.default.white.bold(" ERROR "));
985
- const ts = import_chalk2.default.blackBright(this.ts());
986
- const sep = import_chalk2.default.blackBright("|");
987
- const msg = import_chalk2.default.red(`max crash restarts reached (${import_chalk2.default.bold(String(max))}), giving up`);
988
- console.log(`
989
- ${import_chalk2.default.red("\u2718")} ${badge} ${ts} ${sep} ${msg}
990
- `);
991
- }
992
- /**
993
- * Error — miscellaneous internal error.
994
- */
995
- printError(message) {
996
- const badge = import_ansi_colors2.default.bgRed(import_ansi_colors2.default.white.bold(" ERROR "));
997
- const ts = import_chalk2.default.blackBright(this.ts());
998
- const sep = import_chalk2.default.blackBright("|");
999
- console.error(` ${import_chalk2.default.red("\u2718")} ${badge} ${ts} ${sep} ${import_chalk2.default.red(message)}`);
1000
- }
1001
- /**
1002
- * Stopped — watcher shut down.
1003
- */
1004
- printStopped() {
1005
- const badge = import_ansi_colors2.default.bgBlackBright(import_ansi_colors2.default.white.bold(" STOPPED"));
1006
- const ts = import_chalk2.default.blackBright(this.ts());
1007
- const sep = import_chalk2.default.blackBright("|");
1008
- console.log(` ${import_chalk2.default.gray("\u25A0")} ${badge} ${ts} ${sep} ${import_chalk2.default.gray("watcher stopped")}
1009
- `);
1010
- }
1011
- // ─── Graceful shutdown ────────────────────────────────────────────────────
1012
- setupProcessSignals() {
1013
- const shutdown = async () => {
1014
- console.log("");
1015
- await this.stop();
1016
- process.exit(0);
1017
- };
1018
- process.once("SIGINT", shutdown);
1019
- process.once("SIGTERM", shutdown);
1020
- }
1021
- };
1022
695
  // Annotate the CommonJS export names for ESM import in node:
1023
696
  0 && (module.exports = {
1024
- HotReloadWatcher,
1025
697
  WebServer,
1026
698
  envPath
1027
699
  });