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.cjs +303 -392
- package/dist/index.d.cts +127 -130
- package/dist/index.d.ts +127 -130
- package/dist/index.js +303 -392
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -461,6 +461,244 @@ var getEnvFileLoadingService = (filePath, localLanguage) => {
|
|
|
461
461
|
}
|
|
462
462
|
};
|
|
463
463
|
|
|
464
|
+
// src/hotReload/hotReload.watcher.ts
|
|
465
|
+
var import_fs2 = require("fs");
|
|
466
|
+
var import_promises = require("fs/promises");
|
|
467
|
+
var import_path2 = require("path");
|
|
468
|
+
var import_dotenv = require("dotenv");
|
|
469
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
470
|
+
var import_ansi_colors2 = __toESM(require("ansi-colors"), 1);
|
|
471
|
+
var import_gradient_string = __toESM(require("gradient-string"), 1);
|
|
472
|
+
var DEFAULT_WATCH_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs", ".json"];
|
|
473
|
+
var DEFAULT_HOT_RELOAD_EXTENSIONS = [".json"];
|
|
474
|
+
var DEFAULT_IGNORE = [
|
|
475
|
+
"node_modules",
|
|
476
|
+
"dist",
|
|
477
|
+
".git",
|
|
478
|
+
"package.json",
|
|
479
|
+
"package-lock.json",
|
|
480
|
+
".idea",
|
|
481
|
+
".vscode",
|
|
482
|
+
"coverage",
|
|
483
|
+
"logs"
|
|
484
|
+
];
|
|
485
|
+
var DEFAULT_DEBOUNCE_MS = 300;
|
|
486
|
+
var HotReloadWatcher = class {
|
|
487
|
+
cfg;
|
|
488
|
+
server = null;
|
|
489
|
+
watchers = [];
|
|
490
|
+
debounceTimer = null;
|
|
491
|
+
restarting = false;
|
|
492
|
+
constructor(config) {
|
|
493
|
+
const c = config ?? {};
|
|
494
|
+
this.cfg = {
|
|
495
|
+
rootDir: (0, import_path2.resolve)(c.rootDir ?? process.cwd()),
|
|
496
|
+
watchDirs: c.watchDirs ?? [],
|
|
497
|
+
watchExtensions: c.watchExtensions ?? DEFAULT_WATCH_EXTENSIONS,
|
|
498
|
+
ignore: [...DEFAULT_IGNORE, ...c.ignore ?? []],
|
|
499
|
+
envFile: c.envFile ?? ".env",
|
|
500
|
+
hotReloadExtensions: c.hotReloadExtensions ?? DEFAULT_HOT_RELOAD_EXTENSIONS,
|
|
501
|
+
debounceMs: c.debounceMs ?? DEFAULT_DEBOUNCE_MS
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
// ─── Called by WebServerCore once the HTTP server is listening ────────────
|
|
505
|
+
async attach(server) {
|
|
506
|
+
this.server = server;
|
|
507
|
+
this.printBanner();
|
|
508
|
+
await this.setupWatchers();
|
|
509
|
+
this.setupProcessSignals();
|
|
510
|
+
}
|
|
511
|
+
// ─── File watching ────────────────────────────────────────────────────────
|
|
512
|
+
async setupWatchers() {
|
|
513
|
+
const dirs = [
|
|
514
|
+
this.cfg.rootDir,
|
|
515
|
+
...this.cfg.watchDirs.map((d) => (0, import_path2.resolve)(d))
|
|
516
|
+
];
|
|
517
|
+
for (const dir of dirs) {
|
|
518
|
+
await this.watchRecursive(dir);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async watchRecursive(dir) {
|
|
522
|
+
if (this.shouldIgnoreDir(dir)) return;
|
|
523
|
+
try {
|
|
524
|
+
await (0, import_promises.access)(dir);
|
|
525
|
+
} catch {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const w = (0, import_fs2.watch)(dir, (_, filename) => {
|
|
530
|
+
if (filename) this.onFileChange((0, import_path2.join)(dir, filename));
|
|
531
|
+
});
|
|
532
|
+
w.on("error", () => {
|
|
533
|
+
});
|
|
534
|
+
this.watchers.push(w);
|
|
535
|
+
const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
|
|
536
|
+
for (const entry of entries) {
|
|
537
|
+
if (entry.isDirectory()) await this.watchRecursive((0, import_path2.join)(dir, entry.name));
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// ─── Change handling ──────────────────────────────────────────────────────
|
|
543
|
+
onFileChange(filePath) {
|
|
544
|
+
if (this.shouldIgnoreFile(filePath)) return;
|
|
545
|
+
if (!this.isWatched(filePath)) return;
|
|
546
|
+
if (this.restarting) return;
|
|
547
|
+
const rel = (0, import_path2.relative)(this.cfg.rootDir, filePath);
|
|
548
|
+
if (this.isHotReloadable(filePath)) {
|
|
549
|
+
this.doHotReload(filePath, rel);
|
|
550
|
+
} else {
|
|
551
|
+
this.scheduleRestart(rel);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
doHotReload(filePath, rel) {
|
|
555
|
+
if (this.isEnvFile(filePath)) {
|
|
556
|
+
(0, import_dotenv.config)({ path: filePath, override: true });
|
|
557
|
+
this.printHot(rel, "env variables reloaded");
|
|
558
|
+
} else {
|
|
559
|
+
this.printHot(rel, "json config reloaded");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
scheduleRestart(rel) {
|
|
563
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
564
|
+
this.debounceTimer = setTimeout(() => {
|
|
565
|
+
this.restarting = true;
|
|
566
|
+
this.printReloading(rel);
|
|
567
|
+
this.closeAndExit();
|
|
568
|
+
}, this.cfg.debounceMs);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Gracefully close the HTTP server so in-flight requests can finish,
|
|
572
|
+
* then exit with code 0 — the external runner restarts the process.
|
|
573
|
+
*/
|
|
574
|
+
closeAndExit() {
|
|
575
|
+
if (!this.server) {
|
|
576
|
+
process.exit(0);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
this.server.close(() => {
|
|
580
|
+
process.exit(0);
|
|
581
|
+
});
|
|
582
|
+
setTimeout(() => {
|
|
583
|
+
process.exit(0);
|
|
584
|
+
}, 3e3).unref();
|
|
585
|
+
}
|
|
586
|
+
// ─── Filters ──────────────────────────────────────────────────────────────
|
|
587
|
+
shouldIgnoreDir(dir) {
|
|
588
|
+
return this.cfg.ignore.some(
|
|
589
|
+
(p) => dir.includes(`/${p}`) || dir.endsWith(`/${p}`) || dir.endsWith(p)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
shouldIgnoreFile(filePath) {
|
|
593
|
+
const name = (0, import_path2.basename)(filePath);
|
|
594
|
+
const rel = (0, import_path2.relative)(this.cfg.rootDir, filePath);
|
|
595
|
+
return this.cfg.ignore.some(
|
|
596
|
+
(p) => name === p || rel.includes(p) || filePath.includes(`/${p}/`)
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
isWatched(filePath) {
|
|
600
|
+
if (this.isEnvFile(filePath)) return true;
|
|
601
|
+
return this.cfg.watchExtensions.includes((0, import_path2.extname)(filePath));
|
|
602
|
+
}
|
|
603
|
+
isHotReloadable(filePath) {
|
|
604
|
+
if (this.isEnvFile(filePath)) return true;
|
|
605
|
+
return this.cfg.hotReloadExtensions.includes((0, import_path2.extname)(filePath));
|
|
606
|
+
}
|
|
607
|
+
isEnvFile(filePath) {
|
|
608
|
+
const name = (0, import_path2.basename)(filePath);
|
|
609
|
+
return name === this.cfg.envFile || name.startsWith(".env");
|
|
610
|
+
}
|
|
611
|
+
// ─── ANSI strip (box width calculation) ───────────────────────────────────
|
|
612
|
+
strip(str) {
|
|
613
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
614
|
+
}
|
|
615
|
+
// ─── Timestamp ────────────────────────────────────────────────────────────
|
|
616
|
+
ts() {
|
|
617
|
+
const n = /* @__PURE__ */ new Date();
|
|
618
|
+
return [n.getHours(), n.getMinutes(), n.getSeconds()].map((v) => String(v).padStart(2, "0")).join(":");
|
|
619
|
+
}
|
|
620
|
+
// ─── Console output ───────────────────────────────────────────────────────
|
|
621
|
+
/**
|
|
622
|
+
* Startup banner — same bgGreen box style as CoreService.infoServer().
|
|
623
|
+
*
|
|
624
|
+
* OPTICORE HOT RELOAD ← gradient
|
|
625
|
+
* ████████████████████████████████ ← bgGreen border
|
|
626
|
+
* root ./src
|
|
627
|
+
* watching .ts .js .json .env
|
|
628
|
+
* debounce 300ms
|
|
629
|
+
* ████████████████████████████████ ← bgGreen border
|
|
630
|
+
* watching for changes...
|
|
631
|
+
*/
|
|
632
|
+
printBanner() {
|
|
633
|
+
const rootRel = (0, import_path2.relative)(process.cwd(), this.cfg.rootDir) || ".";
|
|
634
|
+
const extStr = [...this.cfg.watchExtensions, ".env"].join(" ");
|
|
635
|
+
const ignoreParts = this.cfg.ignore.slice(0, 4);
|
|
636
|
+
const ignoreMore = this.cfg.ignore.length > 4 ? ` ${import_chalk2.default.blackBright(`+${this.cfg.ignore.length - 4} more`)}` : "";
|
|
637
|
+
const ignoreStr = import_chalk2.default.blackBright(ignoreParts.join(" ")) + ignoreMore;
|
|
638
|
+
const TITLE = "OPTICORE HOT RELOAD";
|
|
639
|
+
const rows = [
|
|
640
|
+
` root ${import_chalk2.default.white(rootRel)}`,
|
|
641
|
+
` watching ${import_chalk2.default.cyan(extStr)}`,
|
|
642
|
+
` ignoring ${ignoreStr}`,
|
|
643
|
+
` debounce ${import_chalk2.default.white(`${this.cfg.debounceMs}ms`)}`
|
|
644
|
+
];
|
|
645
|
+
const maxLen = Math.max(
|
|
646
|
+
TITLE.length + 4,
|
|
647
|
+
...rows.map((r) => this.strip(r).length)
|
|
648
|
+
) + 4;
|
|
649
|
+
const border = import_chalk2.default.bgGreen.white(" ".repeat(maxLen));
|
|
650
|
+
const titlePad = " ".repeat(Math.max(0, Math.floor((maxLen - TITLE.length) / 2)));
|
|
651
|
+
console.log("\n" + titlePad + (0, import_gradient_string.default)(["#43e97b", "#38f9d7", "#00c6fb"])(TITLE));
|
|
652
|
+
console.log(border);
|
|
653
|
+
for (const row of rows) {
|
|
654
|
+
const pad = Math.max(0, maxLen - this.strip(row).length - 2);
|
|
655
|
+
console.log(import_chalk2.default.bgGreen.white(` ${row}${" ".repeat(pad)} `));
|
|
656
|
+
}
|
|
657
|
+
console.log(border);
|
|
658
|
+
console.log(import_chalk2.default.blackBright(` watching for changes...
|
|
659
|
+
`));
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* ✔ [ HOT ] 14:23:45 | .env → env variables reloaded
|
|
663
|
+
*/
|
|
664
|
+
printHot(file, action) {
|
|
665
|
+
const badge = import_ansi_colors2.default.bgCyan(import_ansi_colors2.default.white.bold(" HOT "));
|
|
666
|
+
const filePart = import_chalk2.default.cyan.bold(file.padEnd(32));
|
|
667
|
+
console.log(
|
|
668
|
+
` ${import_chalk2.default.green("\u2714")} ${badge} ${import_chalk2.default.blackBright(this.ts())} ${import_chalk2.default.blackBright("|")} ${filePart} ${import_chalk2.default.green("\u2192")} ${import_chalk2.default.green(action)}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* ⚡ [ RELOAD ] 14:24:03 | src/routes/user.ts → restarting server...
|
|
673
|
+
*/
|
|
674
|
+
printReloading(file) {
|
|
675
|
+
const badge = import_ansi_colors2.default.bgYellow(import_ansi_colors2.default.white.bold(" RELOAD "));
|
|
676
|
+
const filePart = import_chalk2.default.yellow.bold(file.padEnd(32));
|
|
677
|
+
console.log(
|
|
678
|
+
`
|
|
679
|
+
${import_chalk2.default.yellow("\u26A1")} ${badge} ${import_chalk2.default.blackBright(this.ts())} ${import_chalk2.default.blackBright("|")} ${filePart} ${import_chalk2.default.yellow("\u2192")} ${import_chalk2.default.yellow("restarting server...")}
|
|
680
|
+
`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
// ─── Graceful shutdown on CTRL+C ──────────────────────────────────────────
|
|
684
|
+
setupProcessSignals() {
|
|
685
|
+
const shutdown = () => {
|
|
686
|
+
const badge = import_ansi_colors2.default.bgBlackBright(import_ansi_colors2.default.white.bold(" STOPPED"));
|
|
687
|
+
console.log(
|
|
688
|
+
`
|
|
689
|
+
${import_chalk2.default.gray("\u25A0")} ${badge} ${import_chalk2.default.blackBright(this.ts())} ${import_chalk2.default.blackBright("|")} ${import_chalk2.default.gray("watcher stopped")}
|
|
690
|
+
`
|
|
691
|
+
);
|
|
692
|
+
this.watchers.forEach((w) => {
|
|
693
|
+
w.close();
|
|
694
|
+
});
|
|
695
|
+
process.exit(0);
|
|
696
|
+
};
|
|
697
|
+
process.once("SIGINT", shutdown);
|
|
698
|
+
process.once("SIGTERM", shutdown);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
464
702
|
// src/core/webServer.core.ts
|
|
465
703
|
var WebServerCore = class {
|
|
466
704
|
serverUtility;
|
|
@@ -473,6 +711,7 @@ var WebServerCore = class {
|
|
|
473
711
|
environmentPath;
|
|
474
712
|
serverListenEvent;
|
|
475
713
|
dependenciesRegistered = false;
|
|
714
|
+
hotReloadCfg;
|
|
476
715
|
constructor(paramsConstructor) {
|
|
477
716
|
this.loadTranslationFiles();
|
|
478
717
|
this.stackTraceErrorHandling();
|
|
@@ -489,6 +728,8 @@ var WebServerCore = class {
|
|
|
489
728
|
this.expressApp.use((0, import_cors.default)(paramsConstructor.corsOriginOptions));
|
|
490
729
|
this.serverListenEvent = new import_opticore_catch_exception_error3.ServerListenEventError(paramsConstructor.localLanguage);
|
|
491
730
|
this.serverUtility = new CoreService(paramsConstructor.localLanguage, paramsConstructor.environmentPath);
|
|
731
|
+
const hr = paramsConstructor.hotReload;
|
|
732
|
+
this.hotReloadCfg = !hr ? null : hr === true ? {} : hr;
|
|
492
733
|
}
|
|
493
734
|
/**
|
|
494
735
|
*
|
|
@@ -537,7 +778,7 @@ var WebServerCore = class {
|
|
|
537
778
|
httpCodeValue: import_opticore_http_response4.HttpStatusCode.NOT_FOUND
|
|
538
779
|
});
|
|
539
780
|
} else {
|
|
540
|
-
|
|
781
|
+
const server = this.expressApp.listen(
|
|
541
782
|
Number(this.getEnvironmentValue.appPort),
|
|
542
783
|
() => {
|
|
543
784
|
this.loadTranslationFiles();
|
|
@@ -549,11 +790,16 @@ var WebServerCore = class {
|
|
|
549
790
|
this.container.loadServices();
|
|
550
791
|
this.expressApp.use(import_opticore_express2.express.static(path3.join(import_node_process4.default.cwd(), "public/template")));
|
|
551
792
|
this.registerRoutes(routers);
|
|
793
|
+
const resolvedHmr = this.resolveHotReloadConfig();
|
|
794
|
+
if (resolvedHmr !== null) {
|
|
795
|
+
new HotReloadWatcher(resolvedHmr).attach(server);
|
|
796
|
+
}
|
|
552
797
|
} catch (err) {
|
|
553
798
|
SServerStartError(err, this.environmentPath);
|
|
554
799
|
}
|
|
555
800
|
}
|
|
556
801
|
);
|
|
802
|
+
return server;
|
|
557
803
|
}
|
|
558
804
|
}
|
|
559
805
|
/**
|
|
@@ -597,6 +843,62 @@ var WebServerCore = class {
|
|
|
597
843
|
loadTranslationFiles() {
|
|
598
844
|
loaderTranslationFile(this.localLanguage);
|
|
599
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Resolve the final HotReloadConfig by merging constructor config with
|
|
848
|
+
* HMR environment variables.
|
|
849
|
+
*
|
|
850
|
+
* Priority (highest → lowest):
|
|
851
|
+
* 1. Constructor hotReload properties (explicit code-level config)
|
|
852
|
+
* 2. HMR_* env variables (runtime / per-environment config)
|
|
853
|
+
* 3. HotReloadWatcher internal defaults (built-in fallbacks)
|
|
854
|
+
*
|
|
855
|
+
* The watcher starts when:
|
|
856
|
+
* - constructor passed hotReload: true | HotReloadConfig
|
|
857
|
+
* - OR HMR_ENABLED=true in the .env file
|
|
858
|
+
*/
|
|
859
|
+
resolveHotReloadConfig() {
|
|
860
|
+
const env = this.getEnvironmentValue;
|
|
861
|
+
if (this.hotReloadCfg === null && !env.hmrEnabled) {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
const base = this.hotReloadCfg ?? {};
|
|
865
|
+
const envExtensions = this.hmrExtractExtensions(env.hmrWatchPatterns);
|
|
866
|
+
const envIgnore = this.hmrExtractIgnore(env.hmrIgnorePatterns);
|
|
867
|
+
return {
|
|
868
|
+
rootDir: base.rootDir,
|
|
869
|
+
watchDirs: base.watchDirs,
|
|
870
|
+
watchExtensions: base.watchExtensions ?? (envExtensions.length ? envExtensions : void 0),
|
|
871
|
+
ignore: base.ignore ?? (envIgnore.length ? envIgnore : void 0),
|
|
872
|
+
hotReloadExtensions: base.hotReloadExtensions,
|
|
873
|
+
debounceMs: base.debounceMs ?? (env.hmrDebounceMs > 0 ? env.hmrDebounceMs : void 0),
|
|
874
|
+
envFile: base.envFile
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Extract file extensions from glob patterns such as "src/** /*.ts".
|
|
879
|
+
* "src/** /*.ts"
|
|
880
|
+
* ".env" skipped, handled natively by the watcher
|
|
881
|
+
*/
|
|
882
|
+
hmrExtractExtensions(patterns) {
|
|
883
|
+
const exts = /* @__PURE__ */ new Set();
|
|
884
|
+
for (const p of patterns) {
|
|
885
|
+
const m = p.match(/\*(\.[a-zA-Z0-9]+)$/);
|
|
886
|
+
if (m) {
|
|
887
|
+
exts.add(m[1]);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return [...exts];
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Extract ignore names from glob patterns such as "node_modules/**".
|
|
894
|
+
* "node_modules/**" → "node_modules"
|
|
895
|
+
* "dist/**" → "dist"
|
|
896
|
+
*/
|
|
897
|
+
hmrExtractIgnore(patterns) {
|
|
898
|
+
return [...new Set(
|
|
899
|
+
patterns.map((p) => p.split("/")[0]).filter(Boolean)
|
|
900
|
+
)];
|
|
901
|
+
}
|
|
600
902
|
/**
|
|
601
903
|
*
|
|
602
904
|
* @param allFeatureRoutes
|
|
@@ -628,397 +930,6 @@ var WebServerCore = class {
|
|
|
628
930
|
);
|
|
629
931
|
}
|
|
630
932
|
};
|
|
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
933
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1023
934
|
0 && (module.exports = {
|
|
1024
935
|
HotReloadWatcher,
|