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 +66 -394
- package/dist/index.d.cts +39 -159
- package/dist/index.d.ts +39 -159
- package/dist/index.js +98 -425
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/core/webServer.core.ts
|
|
2
2
|
import * as path3 from "path";
|
|
3
|
-
import
|
|
3
|
+
import process5 from "process";
|
|
4
4
|
import corsOrigin from "cors";
|
|
5
5
|
import { CEventNameError as eventName2, ServerListenEventError as ServerListenEventError2 } from "opticore-catch-exception-error";
|
|
6
6
|
import { express as express2 } from "opticore-express";
|
|
@@ -10,7 +10,7 @@ import { TranslationLoader as TranslationLoader3 } from "opticore-translator";
|
|
|
10
10
|
import { requestCallsEvent } from "opticore-request-call-event";
|
|
11
11
|
|
|
12
12
|
// src/core/handlers/eventProcess.handler.ts
|
|
13
|
-
import
|
|
13
|
+
import process from "process";
|
|
14
14
|
import EventEmitter from "events";
|
|
15
15
|
import { express } from "opticore-express";
|
|
16
16
|
import { ServerListenEventError, CEventNameError as eventName, CEvent as event } from "opticore-catch-exception-error";
|
|
@@ -21,42 +21,42 @@ function eventProcessHandler(localeLanguage) {
|
|
|
21
21
|
errorEmitter.on(eventName.error, (error) => {
|
|
22
22
|
serverListenEvent.listenerError(error);
|
|
23
23
|
});
|
|
24
|
-
|
|
24
|
+
process.on(event.beforeExit, (code) => {
|
|
25
25
|
setTimeout(() => {
|
|
26
26
|
serverListenEvent.processBeforeExit(code);
|
|
27
27
|
}, 100);
|
|
28
28
|
});
|
|
29
|
-
|
|
29
|
+
process.on(event.disconnect, () => {
|
|
30
30
|
serverListenEvent.processDisconnected();
|
|
31
31
|
});
|
|
32
|
-
|
|
32
|
+
process.on(event.exit, (code) => {
|
|
33
33
|
serverListenEvent.exited(code);
|
|
34
34
|
});
|
|
35
|
-
|
|
35
|
+
process.on(event.rejectionHandled, (promise) => {
|
|
36
36
|
serverListenEvent.promiseRejectionHandled(promise);
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
process.on(event.uncaughtException, (error) => {
|
|
39
39
|
serverListenEvent.uncaughtException(error);
|
|
40
40
|
});
|
|
41
|
-
|
|
41
|
+
process.on(event.uncaughtExceptionMonitor, (error) => {
|
|
42
42
|
serverListenEvent.uncaughtExceptionMonitor(error);
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
process.on(event.unhandledRejection, (reason, promise) => {
|
|
45
45
|
serverListenEvent.unhandledRejection(reason, promise);
|
|
46
46
|
});
|
|
47
|
-
|
|
47
|
+
process.on(event.warning, (warning) => {
|
|
48
48
|
serverListenEvent.warning(warning);
|
|
49
49
|
});
|
|
50
|
-
|
|
50
|
+
process.on(event.message, (message) => {
|
|
51
51
|
serverListenEvent.message(message);
|
|
52
52
|
});
|
|
53
|
-
|
|
53
|
+
process.on(event.multipleResolves, (type, promise, reason) => {
|
|
54
54
|
serverListenEvent.multipleResolves(type, promise, reason);
|
|
55
55
|
});
|
|
56
|
-
|
|
56
|
+
process.on(event.sigint, () => {
|
|
57
57
|
serverListenEvent.processInterrupted();
|
|
58
58
|
});
|
|
59
|
-
|
|
59
|
+
process.on(event.sigterm, (signal) => {
|
|
60
60
|
serverListenEvent.sigtermSignalReceived(signal);
|
|
61
61
|
});
|
|
62
62
|
app.use((err, req, res, next) => {
|
|
@@ -65,7 +65,7 @@ function eventProcessHandler(localeLanguage) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// src/application/service/core.service.ts
|
|
68
|
-
import
|
|
68
|
+
import process3 from "process";
|
|
69
69
|
import chalk from "chalk";
|
|
70
70
|
import * as path from "path";
|
|
71
71
|
import * as fs from "fs";
|
|
@@ -75,8 +75,8 @@ import { TranslationLoader } from "opticore-translator";
|
|
|
75
75
|
import { getEnvironmentValue } from "opticore-env-access";
|
|
76
76
|
|
|
77
77
|
// src/utils/envPath.utils.ts
|
|
78
|
-
import
|
|
79
|
-
var envPath =
|
|
78
|
+
import process2 from "process";
|
|
79
|
+
var envPath = process2.cwd() + "/config/env/.env";
|
|
80
80
|
|
|
81
81
|
// src/application/service/loaderTranslationFile.service.ts
|
|
82
82
|
import { translationLoaderConfig } from "opticore-loader-translation";
|
|
@@ -167,7 +167,7 @@ var CoreService = class {
|
|
|
167
167
|
getEnvFileLoading(filePath) {
|
|
168
168
|
this.loadTranslationFiles();
|
|
169
169
|
try {
|
|
170
|
-
const fullPath = path.resolve(
|
|
170
|
+
const fullPath = path.resolve(process3.cwd(), filePath);
|
|
171
171
|
if (fs.existsSync(fullPath)) {
|
|
172
172
|
const env = fs.readFileSync(fullPath, "utf-8");
|
|
173
173
|
const lines = env.split("\n");
|
|
@@ -175,7 +175,7 @@ var CoreService = class {
|
|
|
175
175
|
const match = line.match(/^([^#=]+)=([^#]+)$/);
|
|
176
176
|
if (match) {
|
|
177
177
|
const key = match[1].trim();
|
|
178
|
-
|
|
178
|
+
process3.env[key] = match[2].trim();
|
|
179
179
|
}
|
|
180
180
|
});
|
|
181
181
|
}
|
|
@@ -193,7 +193,7 @@ var CoreService = class {
|
|
|
193
193
|
* Returns an Object containing a node version, openssl, and v0
|
|
194
194
|
*/
|
|
195
195
|
getVersions() {
|
|
196
|
-
const { node, openssl, v8 } =
|
|
196
|
+
const { node, openssl, v8 } = process3.versions;
|
|
197
197
|
const data = {
|
|
198
198
|
"node version": node,
|
|
199
199
|
"openssl": openssl,
|
|
@@ -214,14 +214,14 @@ var CoreService = class {
|
|
|
214
214
|
*/
|
|
215
215
|
getUsageMemory() {
|
|
216
216
|
this.loadTranslationFiles();
|
|
217
|
-
const memoryData =
|
|
217
|
+
const memoryData = process3.memoryUsage();
|
|
218
218
|
const data = {
|
|
219
219
|
[TranslationLoader.t("totalMemoryAllocated", this.localLanguage)]: this.formatMemoryUsage(memoryData.rss),
|
|
220
220
|
[TranslationLoader.t("sizeAllocatedHeap", this.localLanguage)]: this.formatMemoryUsage(memoryData.heapTotal),
|
|
221
221
|
[TranslationLoader.t("memoryUsedExecution", this.localLanguage)]: this.formatMemoryUsage(memoryData.heapUsed),
|
|
222
222
|
[TranslationLoader.t("externalMemory", this.localLanguage)]: this.formatMemoryUsage(memoryData.external),
|
|
223
|
-
[TranslationLoader.t("memoryUsageUser", this.localLanguage)]: this.formatMemoryUsage(
|
|
224
|
-
[TranslationLoader.t("memoryUsageSystem", this.localLanguage)]: this.formatMemoryUsage(
|
|
223
|
+
[TranslationLoader.t("memoryUsageUser", this.localLanguage)]: this.formatMemoryUsage(process3.cpuUsage().user),
|
|
224
|
+
[TranslationLoader.t("memoryUsageSystem", this.localLanguage)]: this.formatMemoryUsage(process3.cpuUsage().system)
|
|
225
225
|
};
|
|
226
226
|
return {
|
|
227
227
|
"rss": data[TranslationLoader.t("totalMemoryAllocated", this.localLanguage)],
|
|
@@ -230,18 +230,18 @@ var CoreService = class {
|
|
|
230
230
|
"external": data[TranslationLoader.t("externalMemory", this.localLanguage)],
|
|
231
231
|
"user": data[TranslationLoader.t("memoryUsageUser", this.localLanguage)],
|
|
232
232
|
"system": data[TranslationLoader.t("memoryUsageSystem", this.localLanguage)],
|
|
233
|
-
"pid":
|
|
233
|
+
"pid": process3.pid
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
237
237
|
* Return an Object containing a project path and running server time
|
|
238
238
|
*/
|
|
239
239
|
getProjectInfo() {
|
|
240
|
-
const startTime =
|
|
241
|
-
const endTime =
|
|
240
|
+
const startTime = process3.hrtime();
|
|
241
|
+
const endTime = process3.hrtime(startTime);
|
|
242
242
|
const executionTime = (endTime[0] * 1e9 + endTime[1]) / 1e6;
|
|
243
243
|
return {
|
|
244
|
-
"projectPath": path.join(
|
|
244
|
+
"projectPath": path.join(process3.cwd()),
|
|
245
245
|
"startingTime": `${executionTime.toFixed(5)} ms`
|
|
246
246
|
};
|
|
247
247
|
}
|
|
@@ -290,7 +290,7 @@ var CoreService = class {
|
|
|
290
290
|
const maxLength = Math.max(...messages.map((m) => {
|
|
291
291
|
return m.replace(/\u001b\[[0-9]{1,2}m/g, "").length;
|
|
292
292
|
})) + 4;
|
|
293
|
-
console.log(chalk.blackBright(`${TranslationLoader.t("tailingServerLog", this.localLanguage)} (${path.join(path.basename(
|
|
293
|
+
console.log(chalk.blackBright(`${TranslationLoader.t("tailingServerLog", this.localLanguage)} (${path.join(path.basename(process3.cwd()), "logs", "app.log")})`));
|
|
294
294
|
const border = chalk.bgGreen.white(" ".repeat(maxLength));
|
|
295
295
|
console.log(border);
|
|
296
296
|
messages.forEach((msg) => {
|
|
@@ -395,7 +395,7 @@ import { SContainer as SContainer3 } from "opticore-dependency-inject";
|
|
|
395
395
|
|
|
396
396
|
// src/application/service/getEnvFileLoading.service.ts
|
|
397
397
|
import path2 from "path";
|
|
398
|
-
import
|
|
398
|
+
import process4 from "process";
|
|
399
399
|
import fs2 from "fs";
|
|
400
400
|
import { TranslationLoader as TranslationLoader2 } from "opticore-translator";
|
|
401
401
|
import { HttpStatusCode as status2 } from "opticore-http-response";
|
|
@@ -403,7 +403,7 @@ var getEnvFileLoadingService = (filePath, localLanguage) => {
|
|
|
403
403
|
loaderTranslationFile(localLanguage);
|
|
404
404
|
const logger = dependenciesContainerProvider(localLanguage).resolve("LoggerCore");
|
|
405
405
|
try {
|
|
406
|
-
const fullPath = path2.resolve(
|
|
406
|
+
const fullPath = path2.resolve(process4.cwd(), filePath);
|
|
407
407
|
if (fs2.existsSync(fullPath)) {
|
|
408
408
|
const env = fs2.readFileSync(fullPath, "utf-8");
|
|
409
409
|
const lines = env.split("\n");
|
|
@@ -411,7 +411,7 @@ var getEnvFileLoadingService = (filePath, localLanguage) => {
|
|
|
411
411
|
const match = line.match(/^([^#=]+)=([^#]+)$/);
|
|
412
412
|
if (match) {
|
|
413
413
|
const key = match[1].trim();
|
|
414
|
-
|
|
414
|
+
process4.env[key] = match[2].trim();
|
|
415
415
|
}
|
|
416
416
|
});
|
|
417
417
|
}
|
|
@@ -427,6 +427,7 @@ var getEnvFileLoadingService = (filePath, localLanguage) => {
|
|
|
427
427
|
};
|
|
428
428
|
|
|
429
429
|
// src/core/webServer.core.ts
|
|
430
|
+
import { HotReloadWatcher } from "opticore-watcher";
|
|
430
431
|
var WebServerCore = class {
|
|
431
432
|
serverUtility;
|
|
432
433
|
expressApp = express2();
|
|
@@ -438,6 +439,7 @@ var WebServerCore = class {
|
|
|
438
439
|
environmentPath;
|
|
439
440
|
serverListenEvent;
|
|
440
441
|
dependenciesRegistered = false;
|
|
442
|
+
hotReloadCfg;
|
|
441
443
|
constructor(paramsConstructor) {
|
|
442
444
|
this.loadTranslationFiles();
|
|
443
445
|
this.stackTraceErrorHandling();
|
|
@@ -454,6 +456,8 @@ var WebServerCore = class {
|
|
|
454
456
|
this.expressApp.use(corsOrigin(paramsConstructor.corsOriginOptions));
|
|
455
457
|
this.serverListenEvent = new ServerListenEventError2(paramsConstructor.localLanguage);
|
|
456
458
|
this.serverUtility = new CoreService(paramsConstructor.localLanguage, paramsConstructor.environmentPath);
|
|
459
|
+
const hr = paramsConstructor.hotReload;
|
|
460
|
+
this.hotReloadCfg = !hr ? null : hr === true ? {} : hr;
|
|
457
461
|
}
|
|
458
462
|
/**
|
|
459
463
|
*
|
|
@@ -502,7 +506,7 @@ var WebServerCore = class {
|
|
|
502
506
|
httpCodeValue: HttpStatusCode3.NOT_FOUND
|
|
503
507
|
});
|
|
504
508
|
} else {
|
|
505
|
-
|
|
509
|
+
const server = this.expressApp.listen(
|
|
506
510
|
Number(this.getEnvironmentValue.appPort),
|
|
507
511
|
() => {
|
|
508
512
|
this.loadTranslationFiles();
|
|
@@ -512,13 +516,18 @@ var WebServerCore = class {
|
|
|
512
516
|
this.registerDependencies(dependenciesProvider);
|
|
513
517
|
}
|
|
514
518
|
this.container.loadServices();
|
|
515
|
-
this.expressApp.use(express2.static(path3.join(
|
|
519
|
+
this.expressApp.use(express2.static(path3.join(process5.cwd(), "public/template")));
|
|
516
520
|
this.registerRoutes(routers);
|
|
521
|
+
const resolvedHmr = this.resolveHotReloadConfig();
|
|
522
|
+
if (resolvedHmr !== null) {
|
|
523
|
+
new HotReloadWatcher(resolvedHmr).attach(server);
|
|
524
|
+
}
|
|
517
525
|
} catch (err) {
|
|
518
526
|
SServerStartError(err, this.environmentPath);
|
|
519
527
|
}
|
|
520
528
|
}
|
|
521
529
|
);
|
|
530
|
+
return server;
|
|
522
531
|
}
|
|
523
532
|
}
|
|
524
533
|
/**
|
|
@@ -562,6 +571,62 @@ var WebServerCore = class {
|
|
|
562
571
|
loadTranslationFiles() {
|
|
563
572
|
loaderTranslationFile(this.localLanguage);
|
|
564
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Resolve the final HotReloadConfig by merging constructor config with
|
|
576
|
+
* HMR environment variables.
|
|
577
|
+
*
|
|
578
|
+
* Priority (highest → lowest):
|
|
579
|
+
* 1. Constructor hotReload properties (explicit code-level config)
|
|
580
|
+
* 2. HMR_* env variables (runtime / per-environment config)
|
|
581
|
+
* 3. HotReloadWatcher internal defaults (built-in fallbacks)
|
|
582
|
+
*
|
|
583
|
+
* The watcher starts when:
|
|
584
|
+
* - constructor passed hotReload: true | HotReloadConfig
|
|
585
|
+
* - OR HMR_ENABLED=true in the .env file
|
|
586
|
+
*/
|
|
587
|
+
resolveHotReloadConfig() {
|
|
588
|
+
const env = this.getEnvironmentValue;
|
|
589
|
+
if (this.hotReloadCfg === null && !env.hmrEnabled) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const base = this.hotReloadCfg ?? {};
|
|
593
|
+
const envExtensions = this.hmrExtractExtensions(env.hmrWatchPatterns);
|
|
594
|
+
const envIgnore = this.hmrExtractIgnore(env.hmrIgnorePatterns);
|
|
595
|
+
return {
|
|
596
|
+
rootDir: base.rootDir,
|
|
597
|
+
watchDirs: base.watchDirs,
|
|
598
|
+
watchExtensions: base.watchExtensions ?? (envExtensions.length ? envExtensions : void 0),
|
|
599
|
+
ignore: base.ignore ?? (envIgnore.length ? envIgnore : void 0),
|
|
600
|
+
hotReloadExtensions: base.hotReloadExtensions,
|
|
601
|
+
debounceMs: base.debounceMs ?? (env.hmrDebounceMs > 0 ? env.hmrDebounceMs : void 0),
|
|
602
|
+
envFile: base.envFile
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Extract file extensions from glob patterns such as "src/** /*.ts".
|
|
607
|
+
* "src/** /*.ts"
|
|
608
|
+
* ".env" skipped, handled natively by the watcher
|
|
609
|
+
*/
|
|
610
|
+
hmrExtractExtensions(patterns) {
|
|
611
|
+
const exts = /* @__PURE__ */ new Set();
|
|
612
|
+
for (const p of patterns) {
|
|
613
|
+
const m = p.match(/\*(\.[a-zA-Z0-9]+)$/);
|
|
614
|
+
if (m) {
|
|
615
|
+
exts.add(m[1]);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return [...exts];
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Extract ignore names from glob patterns such as "node_modules/**".
|
|
622
|
+
* "node_modules/**" → "node_modules"
|
|
623
|
+
* "dist/**" → "dist"
|
|
624
|
+
*/
|
|
625
|
+
hmrExtractIgnore(patterns) {
|
|
626
|
+
return [...new Set(
|
|
627
|
+
patterns.map((p) => p.split("/")[0]).filter(Boolean)
|
|
628
|
+
)];
|
|
629
|
+
}
|
|
565
630
|
/**
|
|
566
631
|
*
|
|
567
632
|
* @param allFeatureRoutes
|
|
@@ -593,399 +658,7 @@ var WebServerCore = class {
|
|
|
593
658
|
);
|
|
594
659
|
}
|
|
595
660
|
};
|
|
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
661
|
export {
|
|
988
|
-
HotReloadWatcher,
|
|
989
662
|
WebServerCore as WebServer,
|
|
990
663
|
envPath
|
|
991
664
|
};
|