opticore-webapp 1.0.70 → 1.0.71

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
@@ -1,16 +1,16 @@
1
1
  // src/core/webServer.core.ts
2
- import * as path2 from "path";
3
- import process4 from "process";
2
+ import * as path3 from "path";
3
+ import process6 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";
7
7
  import { getEnvironmentValue as getEnvironmentValue2 } from "opticore-env-access";
8
8
  import { HttpStatusCode as HttpStatusCode3 } from "opticore-http-response";
9
- import { TranslationLoader as TranslationLoader2 } from "opticore-translator";
9
+ 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 process from "process";
13
+ import process2 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
- process.on(event.beforeExit, (code) => {
24
+ process2.on(event.beforeExit, (code) => {
25
25
  setTimeout(() => {
26
26
  serverListenEvent.processBeforeExit(code);
27
27
  }, 100);
28
28
  });
29
- process.on(event.disconnect, () => {
29
+ process2.on(event.disconnect, () => {
30
30
  serverListenEvent.processDisconnected();
31
31
  });
32
- process.on(event.exit, (code) => {
32
+ process2.on(event.exit, (code) => {
33
33
  serverListenEvent.exited(code);
34
34
  });
35
- process.on(event.rejectionHandled, (promise) => {
35
+ process2.on(event.rejectionHandled, (promise) => {
36
36
  serverListenEvent.promiseRejectionHandled(promise);
37
37
  });
38
- process.on(event.uncaughtException, (error) => {
38
+ process2.on(event.uncaughtException, (error) => {
39
39
  serverListenEvent.uncaughtException(error);
40
40
  });
41
- process.on(event.uncaughtExceptionMonitor, (error) => {
41
+ process2.on(event.uncaughtExceptionMonitor, (error) => {
42
42
  serverListenEvent.uncaughtExceptionMonitor(error);
43
43
  });
44
- process.on(event.unhandledRejection, (reason, promise) => {
44
+ process2.on(event.unhandledRejection, (reason, promise) => {
45
45
  serverListenEvent.unhandledRejection(reason, promise);
46
46
  });
47
- process.on(event.warning, (warning) => {
47
+ process2.on(event.warning, (warning) => {
48
48
  serverListenEvent.warning(warning);
49
49
  });
50
- process.on(event.message, (message) => {
50
+ process2.on(event.message, (message) => {
51
51
  serverListenEvent.message(message);
52
52
  });
53
- process.on(event.multipleResolves, (type, promise, reason) => {
53
+ process2.on(event.multipleResolves, (type, promise, reason) => {
54
54
  serverListenEvent.multipleResolves(type, promise, reason);
55
55
  });
56
- process.on(event.sigint, () => {
56
+ process2.on(event.sigint, () => {
57
57
  serverListenEvent.processInterrupted();
58
58
  });
59
- process.on(event.sigterm, (signal) => {
59
+ process2.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 process3 from "process";
68
+ import process4 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 process2 from "process";
79
- var envPath = process2.cwd() + "/config/env/.env";
78
+ import process3 from "process";
79
+ var envPath = process3.cwd() + "/config/env/.env";
80
80
 
81
81
  // src/application/service/loaderTranslationFile.service.ts
82
82
  import { translationLoaderConfig } from "opticore-loader-translation";
@@ -112,16 +112,35 @@ var dependenciesContainerProvider = (localLang) => {
112
112
  return new SContainer(localLang, dependencies);
113
113
  };
114
114
 
115
+ // src/application/service/core.service.ts
116
+ import { SContainer as SContainer2 } from "opticore-dependency-inject";
117
+
118
+ // src/utils/parsingDateTime.utils.ts
119
+ var parsingDateTimeUtils = (localFormatTime) => {
120
+ const now = /* @__PURE__ */ new Date();
121
+ const formatter = new Intl.DateTimeFormat(localFormatTime, {
122
+ day: "2-digit",
123
+ month: "2-digit",
124
+ year: "numeric",
125
+ hour: "2-digit",
126
+ minute: "2-digit",
127
+ hour12: false
128
+ });
129
+ return formatter.format(now);
130
+ };
131
+
115
132
  // src/application/service/core.service.ts
116
133
  var CoreService = class {
117
134
  localLanguage;
118
135
  environmentPath;
119
136
  serverLog;
120
137
  logger;
138
+ container;
121
139
  constructor(localLang, environmentPath) {
122
140
  this.loadTranslationFiles();
123
141
  this.environmentPath = environmentPath;
124
142
  this.localLanguage = localLang;
143
+ this.container = new SContainer2(localLang);
125
144
  this.serverLog = dependenciesContainerProvider(localLang).resolve("OpticoreLogger");
126
145
  this.logger = dependenciesContainerProvider(localLang).resolve("LoggerCore");
127
146
  }
@@ -148,7 +167,7 @@ var CoreService = class {
148
167
  getEnvFileLoading(filePath) {
149
168
  this.loadTranslationFiles();
150
169
  try {
151
- const fullPath = path.resolve(process3.cwd(), filePath);
170
+ const fullPath = path.resolve(process4.cwd(), filePath);
152
171
  if (fs.existsSync(fullPath)) {
153
172
  const env = fs.readFileSync(fullPath, "utf-8");
154
173
  const lines = env.split("\n");
@@ -156,7 +175,7 @@ var CoreService = class {
156
175
  const match = line.match(/^([^#=]+)=([^#]+)$/);
157
176
  if (match) {
158
177
  const key = match[1].trim();
159
- process3.env[key] = match[2].trim();
178
+ process4.env[key] = match[2].trim();
160
179
  }
161
180
  });
162
181
  }
@@ -174,7 +193,7 @@ var CoreService = class {
174
193
  * Returns an Object containing a node version, openssl, and v0
175
194
  */
176
195
  getVersions() {
177
- const { node, openssl, v8 } = process3.versions;
196
+ const { node, openssl, v8 } = process4.versions;
178
197
  const data = {
179
198
  "node version": node,
180
199
  "openssl": openssl,
@@ -195,14 +214,14 @@ var CoreService = class {
195
214
  */
196
215
  getUsageMemory() {
197
216
  this.loadTranslationFiles();
198
- const memoryData = process3.memoryUsage();
217
+ const memoryData = process4.memoryUsage();
199
218
  const data = {
200
219
  [TranslationLoader.t("totalMemoryAllocated", this.localLanguage)]: this.formatMemoryUsage(memoryData.rss),
201
220
  [TranslationLoader.t("sizeAllocatedHeap", this.localLanguage)]: this.formatMemoryUsage(memoryData.heapTotal),
202
221
  [TranslationLoader.t("memoryUsedExecution", this.localLanguage)]: this.formatMemoryUsage(memoryData.heapUsed),
203
222
  [TranslationLoader.t("externalMemory", this.localLanguage)]: this.formatMemoryUsage(memoryData.external),
204
- [TranslationLoader.t("memoryUsageUser", this.localLanguage)]: this.formatMemoryUsage(process3.cpuUsage().user),
205
- [TranslationLoader.t("memoryUsageSystem", this.localLanguage)]: this.formatMemoryUsage(process3.cpuUsage().system)
223
+ [TranslationLoader.t("memoryUsageUser", this.localLanguage)]: this.formatMemoryUsage(process4.cpuUsage().user),
224
+ [TranslationLoader.t("memoryUsageSystem", this.localLanguage)]: this.formatMemoryUsage(process4.cpuUsage().system)
206
225
  };
207
226
  return {
208
227
  "rss": data[TranslationLoader.t("totalMemoryAllocated", this.localLanguage)],
@@ -211,18 +230,18 @@ var CoreService = class {
211
230
  "external": data[TranslationLoader.t("externalMemory", this.localLanguage)],
212
231
  "user": data[TranslationLoader.t("memoryUsageUser", this.localLanguage)],
213
232
  "system": data[TranslationLoader.t("memoryUsageSystem", this.localLanguage)],
214
- "pid": process3.pid
233
+ "pid": process4.pid
215
234
  };
216
235
  }
217
236
  /**
218
237
  * Return an Object containing a project path and running server time
219
238
  */
220
239
  getProjectInfo() {
221
- const startTime = process3.hrtime();
222
- const endTime = process3.hrtime(startTime);
240
+ const startTime = process4.hrtime();
241
+ const endTime = process4.hrtime(startTime);
223
242
  const executionTime = (endTime[0] * 1e9 + endTime[1]) / 1e6;
224
243
  return {
225
- "projectPath": path.join(process3.cwd()),
244
+ "projectPath": path.join(process4.cwd()),
226
245
  "startingTime": `${executionTime.toFixed(5)} ms`
227
246
  };
228
247
  }
@@ -261,7 +280,7 @@ var CoreService = class {
261
280
  this.loadTranslationFiles();
262
281
  try {
263
282
  const getEnvironment = getEnvironmentValue(this.environmentPath);
264
- const msg5 = getEnvironment.protocolTransfert === "" ? colors.underline(`http://${host}:${port}`) : colors.underline(`${getEnvironment.protocolTransfert}://${host}:${port}`);
283
+ const msg5 = getEnvironment.protocolTransfer === "" ? colors.underline(`http://${host}:${port}`) : colors.underline(`${getEnvironment.protocolTransfer}://${host}:${port}`);
265
284
  const messages = [
266
285
  TranslationLoader.t("webServerListening", this.localLanguage),
267
286
  TranslationLoader.t("webServerUsingNodeVersion", this.localLanguage, { nodeVersion: this.getVersions().nodeVersion }),
@@ -271,7 +290,7 @@ var CoreService = class {
271
290
  const maxLength = Math.max(...messages.map((m) => {
272
291
  return m.replace(/\u001b\[[0-9]{1,2}m/g, "").length;
273
292
  })) + 4;
274
- console.log(chalk.blackBright(`${TranslationLoader.t("tailingServerLog", this.localLanguage)} (${path.join(path.basename(process3.cwd()), "logs", "app.log")})`));
293
+ console.log(chalk.blackBright(`${TranslationLoader.t("tailingServerLog", this.localLanguage)} (${path.join(path.basename(process4.cwd()), "logs", "app.log")})`));
275
294
  const border = chalk.bgGreen.white(" ".repeat(maxLength));
276
295
  console.log(border);
277
296
  messages.forEach((msg) => {
@@ -281,17 +300,31 @@ var CoreService = class {
281
300
  });
282
301
  console.log(border);
283
302
  console.log("\n");
284
- this.logger.success({
285
- title: TranslationLoader.t("serverRunningTitle", this.localLanguage),
286
- message: TranslationLoader.t("serverRunningAt", this.localLanguage, { server: msg5 })
287
- });
288
303
  this.serverLog.serverLog({
289
- timestamp: (/* @__PURE__ */ new Date()).toString(),
304
+ timestamp: parsingDateTimeUtils(getEnvironment.localFormatTime),
290
305
  level: "SERVER",
291
306
  title: TranslationLoader.t("serverRunningTitle", this.localLanguage),
292
307
  typeName: TranslationLoader.t("opticoreServerTypeName", this.localLanguage),
293
308
  message: TranslationLoader.t("serverRunningAt", this.localLanguage, { server: msg5 })
294
309
  });
310
+ const dependenciesList = this.container.listDependencies();
311
+ if (dependenciesList.length > 0) {
312
+ this.serverLog.opticoreLog({
313
+ level: "OPTICORE",
314
+ message: TranslationLoader.t("DEPENDENCIES_STORED", this.localLanguage, { dependenciesList: dependenciesList.join(", ") }),
315
+ timestamp: parsingDateTimeUtils(getEnvironment.localFormatTime),
316
+ title: TranslationLoader.t("DEPENDENCIES_CONTAINER", this.localLanguage),
317
+ typeName: "DEPENDENCIES_CONTAINER"
318
+ });
319
+ } else {
320
+ this.serverLog.opticoreLog({
321
+ level: "OPTICORE",
322
+ message: TranslationLoader.t("NO_DEPENDENCIES_STORED", this.localLanguage),
323
+ timestamp: parsingDateTimeUtils(getEnvironment.localFormatTime),
324
+ title: TranslationLoader.t("DEPENDENCIES_CONTAINER", this.localLanguage),
325
+ typeName: "DEPENDENCIES_CONTAINER"
326
+ });
327
+ }
295
328
  } catch (err) {
296
329
  this.logger.error({
297
330
  message: err.message,
@@ -358,10 +391,46 @@ var SServerStartError = (err, environmentPath) => {
358
391
  };
359
392
 
360
393
  // src/core/webServer.core.ts
361
- import { SContainer as SContainer2 } from "opticore-dependency-inject";
394
+ import { SContainer as SContainer3 } from "opticore-dependency-inject";
395
+
396
+ // src/application/service/getEnvFileLoading.service.ts
397
+ import path2 from "path";
398
+ import process5 from "process";
399
+ import fs2 from "fs";
400
+ import { TranslationLoader as TranslationLoader2 } from "opticore-translator";
401
+ import { HttpStatusCode as status2 } from "opticore-http-response";
402
+ var getEnvFileLoadingService = (filePath, localLanguage) => {
403
+ loaderTranslationFile(localLanguage);
404
+ const logger = dependenciesContainerProvider(localLanguage).resolve("LoggerCore");
405
+ try {
406
+ const fullPath = path2.resolve(process5.cwd(), filePath);
407
+ if (fs2.existsSync(fullPath)) {
408
+ const env = fs2.readFileSync(fullPath, "utf-8");
409
+ const lines = env.split("\n");
410
+ lines.forEach((line) => {
411
+ const match = line.match(/^([^#=]+)=([^#]+)$/);
412
+ if (match) {
413
+ const key = match[1].trim();
414
+ process5.env[key] = match[2].trim();
415
+ }
416
+ });
417
+ }
418
+ } catch (err) {
419
+ logger.error({
420
+ message: err.message,
421
+ title: TranslationLoader2.t("EnvFileLoading", localLanguage),
422
+ errorType: err.code,
423
+ stackTrace: err.stack,
424
+ httpCodeValue: status2.INTERNAL_SERVER_ERROR
425
+ });
426
+ }
427
+ };
428
+
429
+ // src/core/webServer.core.ts
362
430
  var WebServerCore = class {
363
431
  serverUtility;
364
432
  expressApp = express2();
433
+ container;
365
434
  localLanguage;
366
435
  loggerConfig;
367
436
  routerExpressApp;
@@ -377,6 +446,7 @@ var WebServerCore = class {
377
446
  this.loggerConfig = paramsConstructor.loggerConfig;
378
447
  this.localLanguage = paramsConstructor.localLanguage;
379
448
  this.environmentPath = paramsConstructor.environmentPath;
449
+ this.container = new SContainer3(paramsConstructor.localLanguage);
380
450
  this.expressApp.use(express2.json());
381
451
  this.expressApp.use(express2.raw());
382
452
  this.expressApp.use(express2.text());
@@ -385,16 +455,17 @@ var WebServerCore = class {
385
455
  this.serverListenEvent = new ServerListenEventError2(paramsConstructor.localLanguage);
386
456
  this.serverUtility = new CoreService(paramsConstructor.localLanguage, paramsConstructor.environmentPath);
387
457
  }
458
+ /**
459
+ *
460
+ * @param dependencies
461
+ */
388
462
  registerDependencies(dependencies2) {
463
+ this.loadTranslationFiles();
389
464
  try {
465
+ getEnvFileLoadingService(".env", this.localLanguage);
390
466
  if (dependencies2 && dependencies2.length > 0) {
391
467
  dependencies2.forEach((dependency) => {
392
- return new SContainer2(this.localLanguage).register(dependency.key, dependency.factory, dependency.scope);
393
- });
394
- } else {
395
- SLogger(this.environmentPath).logger.info({
396
- message: TranslationLoader2.t("NO_REGISTER_DEPENDENCIES", this.localLanguage),
397
- title: TranslationLoader2.t("REGISTER_DEPENDENCIES", this.localLanguage)
468
+ this.container.register(dependency.key, dependency.factory, dependency.scope);
398
469
  });
399
470
  }
400
471
  } catch (err) {
@@ -407,8 +478,15 @@ var WebServerCore = class {
407
478
  });
408
479
  }
409
480
  }
481
+ /**
482
+ *
483
+ * @param routers
484
+ * @param databaseCallback
485
+ * @param dependenciesProvider
486
+ */
410
487
  onStartServer(routers, databaseCallback, dependenciesProvider) {
411
488
  this.loadTranslationFiles();
489
+ getEnvFileLoadingService(".env", this.localLanguage);
412
490
  if (this.getEnvironmentValue.appPort === "" && Number(this.getEnvironmentValue.appPort) === 0) {
413
491
  this.serverListenEvent.hostPortUndefined(Number(this.getEnvironmentValue.appPort));
414
492
  } else if (this.getEnvironmentValue.appHost === "") {
@@ -417,9 +495,9 @@ var WebServerCore = class {
417
495
  this.serverListenEvent.portUndefined();
418
496
  } else if (this.localLanguage === "") {
419
497
  SLogger(this.environmentPath).logger.error({
420
- message: TranslationLoader2.t("noDefaultLocalLang", this.localLanguage),
421
- title: TranslationLoader2.t("noLocalLang", this.localLanguage),
422
- errorType: TranslationLoader2.t("localLangMissing", this.localLanguage),
498
+ message: TranslationLoader3.t("noDefaultLocalLang", this.localLanguage),
499
+ title: TranslationLoader3.t("noLocalLang", this.localLanguage),
500
+ errorType: TranslationLoader3.t("localLangMissing", this.localLanguage),
423
501
  stackTrace: void 0,
424
502
  httpCodeValue: HttpStatusCode3.NOT_FOUND
425
503
  });
@@ -430,8 +508,11 @@ var WebServerCore = class {
430
508
  this.loadTranslationFiles();
431
509
  try {
432
510
  databaseCallback(this.getEnvironmentValue);
433
- new SContainer2(this.localLanguage, dependenciesProvider);
434
- this.expressApp.use(express2.static(path2.join(process4.cwd(), "public/template")));
511
+ if (dependenciesProvider) {
512
+ this.registerDependencies(dependenciesProvider);
513
+ }
514
+ this.container.loadServices();
515
+ this.expressApp.use(express2.static(path3.join(process6.cwd(), "public/template")));
435
516
  this.registerRoutes(routers);
436
517
  } catch (err) {
437
518
  SServerStartError(err, this.environmentPath);
@@ -440,6 +521,10 @@ var WebServerCore = class {
440
521
  );
441
522
  }
442
523
  }
524
+ /**
525
+ *
526
+ * @param serverWeb
527
+ */
443
528
  onListeningOnServerEvent(serverWeb) {
444
529
  this.loadTranslationFiles();
445
530
  serverWeb.on(eventName2.error, (err) => {
@@ -452,6 +537,10 @@ var WebServerCore = class {
452
537
  this.infoWebApp();
453
538
  });
454
539
  }
540
+ /**
541
+ *
542
+ * @param serverWeb
543
+ */
455
544
  onRequestOnServerEvent(serverWeb) {
456
545
  this.loadTranslationFiles();
457
546
  serverWeb.on(eventName2.request, (req, res) => {
@@ -504,7 +593,399 @@ var WebServerCore = class {
504
593
  );
505
594
  }
506
595
  };
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
+ };
507
987
  export {
988
+ HotReloadWatcher,
508
989
  WebServerCore as WebServer,
509
990
  envPath
510
991
  };