@expressots/adapter-express 3.0.0 → 4.0.0-preview.3

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.
Files changed (244) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +61 -118
  3. package/lib/CHANGELOG.md +36 -5
  4. package/lib/README.md +61 -118
  5. package/lib/cjs/adapter-express/application-express.base.js +3 -1
  6. package/lib/cjs/adapter-express/application-express.js +1405 -85
  7. package/lib/cjs/adapter-express/express-utils/conditional-middleware.js +102 -0
  8. package/lib/cjs/adapter-express/express-utils/constants.js +17 -0
  9. package/lib/cjs/adapter-express/express-utils/content-negotiation-decorators.js +129 -0
  10. package/lib/cjs/adapter-express/express-utils/decorators.js +225 -59
  11. package/lib/cjs/adapter-express/express-utils/exception-filter-decorators.js +11 -0
  12. package/lib/cjs/adapter-express/express-utils/guard-context-factory.js +84 -0
  13. package/lib/cjs/adapter-express/express-utils/guard-middleware.js +115 -0
  14. package/lib/cjs/adapter-express/express-utils/guard-utils.js +18 -0
  15. package/lib/cjs/adapter-express/express-utils/http-context-store.js +15 -0
  16. package/lib/cjs/adapter-express/express-utils/http-status-middleware.js +37 -2
  17. package/lib/cjs/adapter-express/express-utils/index.js +67 -1
  18. package/lib/cjs/adapter-express/express-utils/interceptor-middleware.js +132 -0
  19. package/lib/cjs/adapter-express/express-utils/inversify-express-server.js +827 -64
  20. package/lib/cjs/adapter-express/express-utils/lazy-module-middleware.js +241 -0
  21. package/lib/cjs/adapter-express/express-utils/middleware-composition.js +95 -0
  22. package/lib/cjs/adapter-express/express-utils/path-pattern-compat.js +129 -0
  23. package/lib/cjs/adapter-express/express-utils/permission-preloader.middleware.js +48 -0
  24. package/lib/cjs/adapter-express/express-utils/route-constraints.js +104 -0
  25. package/lib/cjs/adapter-express/express-utils/scope-extractor.interface.js +2 -0
  26. package/lib/cjs/adapter-express/express-utils/scope-extractor.js +66 -0
  27. package/lib/cjs/adapter-express/express-utils/setup-authorization.js +71 -0
  28. package/lib/cjs/adapter-express/express-utils/setup-event-system.js +113 -0
  29. package/lib/cjs/adapter-express/express-utils/setup-interceptors.js +103 -0
  30. package/lib/cjs/adapter-express/express-utils/setup-lazy-loading.js +228 -0
  31. package/lib/cjs/adapter-express/express-utils/utils.js +30 -12
  32. package/lib/cjs/adapter-express/express-utils/validation-decorators.js +205 -0
  33. package/lib/cjs/adapter-express/express-utils/validation-service.js +252 -0
  34. package/lib/cjs/adapter-express/index.js +7 -5
  35. package/lib/cjs/adapter-express/micro-api/application-express-micro-route.js +31 -1
  36. package/lib/cjs/adapter-express/micro-api/application-express-micro.js +8 -38
  37. package/lib/cjs/adapter-express/micro-api/gateway/circuit-breaker.js +174 -0
  38. package/lib/cjs/adapter-express/micro-api/gateway/index.js +11 -0
  39. package/lib/cjs/adapter-express/micro-api/gateway/service-proxy.js +214 -0
  40. package/lib/cjs/adapter-express/micro-api/index.js +27 -3
  41. package/lib/cjs/adapter-express/micro-api/micro.js +272 -0
  42. package/lib/cjs/adapter-express/micro-api/queue/index.js +8 -0
  43. package/lib/cjs/adapter-express/micro-api/queue/queue.interface.js +2 -0
  44. package/lib/cjs/adapter-express/micro-api/queue/rabbitmq-consumer.js +255 -0
  45. package/lib/cjs/adapter-express/micro-api/serverless/aws-lambda.adapter.js +183 -0
  46. package/lib/cjs/adapter-express/micro-api/serverless/cloudflare.adapter.js +158 -0
  47. package/lib/cjs/adapter-express/micro-api/serverless/index.js +12 -0
  48. package/lib/cjs/adapter-express/micro-api/serverless/vercel.adapter.js +102 -0
  49. package/lib/cjs/adapter-express/micro-api/service-mesh/index.js +10 -0
  50. package/lib/cjs/adapter-express/micro-api/service-mesh/service-client.js +194 -0
  51. package/lib/cjs/adapter-express/micro-api/service-mesh/service-discovery.js +261 -0
  52. package/lib/cjs/adapter-express/middleware/index.js +21 -0
  53. package/lib/cjs/adapter-express/middleware/request-logging.middleware.js +244 -0
  54. package/lib/cjs/adapter-express/render/engine.js +15 -15
  55. package/lib/cjs/adapter-express/render/index.js +5 -0
  56. package/lib/cjs/adapter-express/studio/index.js +10 -0
  57. package/lib/cjs/adapter-express/studio/studio-integration.js +267 -0
  58. package/lib/cjs/index.js +1 -1
  59. package/lib/cjs/types/adapter-express/application-express.base.d.ts +20 -7
  60. package/lib/cjs/types/adapter-express/application-express.d.ts +316 -33
  61. package/lib/cjs/types/adapter-express/express-utils/base-middleware.d.ts +2 -2
  62. package/lib/cjs/types/adapter-express/express-utils/conditional-middleware.d.ts +97 -0
  63. package/lib/cjs/types/adapter-express/express-utils/constants.d.ts +13 -0
  64. package/lib/cjs/types/adapter-express/express-utils/content-negotiation-decorators.d.ts +94 -0
  65. package/lib/cjs/types/adapter-express/express-utils/decorators.d.ts +54 -6
  66. package/lib/cjs/types/adapter-express/express-utils/exception-filter-decorators.d.ts +6 -0
  67. package/lib/cjs/types/adapter-express/express-utils/guard-context-factory.d.ts +17 -0
  68. package/lib/cjs/types/adapter-express/express-utils/guard-middleware.d.ts +22 -0
  69. package/lib/cjs/types/adapter-express/express-utils/guard-utils.d.ts +11 -0
  70. package/lib/cjs/types/adapter-express/express-utils/http-context-store.d.ts +20 -0
  71. package/lib/cjs/types/adapter-express/express-utils/httpResponseMessage.d.ts +1 -1
  72. package/lib/cjs/types/adapter-express/express-utils/index.d.ts +30 -2
  73. package/lib/cjs/types/adapter-express/express-utils/interceptor-middleware.d.ts +40 -0
  74. package/lib/cjs/types/adapter-express/express-utils/interfaces.d.ts +42 -5
  75. package/lib/cjs/types/adapter-express/express-utils/inversify-express-server.d.ts +114 -2
  76. package/lib/cjs/types/adapter-express/express-utils/lazy-module-middleware.d.ts +122 -0
  77. package/lib/cjs/types/adapter-express/express-utils/middleware-composition.d.ts +85 -0
  78. package/lib/cjs/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
  79. package/lib/cjs/types/adapter-express/express-utils/permission-preloader.middleware.d.ts +10 -0
  80. package/lib/cjs/types/adapter-express/express-utils/route-constraints.d.ts +98 -0
  81. package/lib/cjs/types/adapter-express/express-utils/scope-extractor.d.ts +21 -0
  82. package/lib/cjs/types/adapter-express/express-utils/scope-extractor.interface.d.ts +12 -0
  83. package/lib/cjs/types/adapter-express/express-utils/setup-authorization.d.ts +34 -0
  84. package/lib/cjs/types/adapter-express/express-utils/setup-event-system.d.ts +118 -0
  85. package/lib/cjs/types/adapter-express/express-utils/setup-interceptors.d.ts +115 -0
  86. package/lib/cjs/types/adapter-express/express-utils/setup-lazy-loading.d.ts +123 -0
  87. package/lib/cjs/types/adapter-express/express-utils/utils.d.ts +17 -2
  88. package/lib/cjs/types/adapter-express/express-utils/validation-decorators.d.ts +145 -0
  89. package/lib/cjs/types/adapter-express/express-utils/validation-service.d.ts +88 -0
  90. package/lib/cjs/types/adapter-express/index.d.ts +6 -4
  91. package/lib/cjs/types/adapter-express/micro-api/application-express-micro-route.d.ts +25 -14
  92. package/lib/cjs/types/adapter-express/micro-api/application-express-micro.d.ts +3 -10
  93. package/lib/cjs/types/adapter-express/micro-api/gateway/circuit-breaker.d.ts +111 -0
  94. package/lib/cjs/types/adapter-express/micro-api/gateway/index.d.ts +5 -0
  95. package/lib/cjs/types/adapter-express/micro-api/gateway/service-proxy.d.ts +83 -0
  96. package/lib/cjs/types/adapter-express/micro-api/index.d.ts +7 -1
  97. package/lib/cjs/types/adapter-express/micro-api/micro.d.ts +83 -0
  98. package/lib/cjs/types/adapter-express/micro-api/queue/index.d.ts +5 -0
  99. package/lib/cjs/types/adapter-express/micro-api/queue/queue.interface.d.ts +60 -0
  100. package/lib/cjs/types/adapter-express/micro-api/queue/rabbitmq-consumer.d.ts +86 -0
  101. package/lib/cjs/types/adapter-express/micro-api/serverless/aws-lambda.adapter.d.ts +77 -0
  102. package/lib/cjs/types/adapter-express/micro-api/serverless/cloudflare.adapter.d.ts +64 -0
  103. package/lib/cjs/types/adapter-express/micro-api/serverless/index.d.ts +6 -0
  104. package/lib/cjs/types/adapter-express/micro-api/serverless/vercel.adapter.d.ts +56 -0
  105. package/lib/cjs/types/adapter-express/micro-api/service-mesh/index.d.ts +5 -0
  106. package/lib/cjs/types/adapter-express/micro-api/service-mesh/service-client.d.ts +122 -0
  107. package/lib/cjs/types/adapter-express/micro-api/service-mesh/service-discovery.d.ts +150 -0
  108. package/lib/cjs/types/adapter-express/middleware/index.d.ts +5 -0
  109. package/lib/cjs/types/adapter-express/middleware/request-logging.middleware.d.ts +65 -0
  110. package/lib/cjs/types/adapter-express/render/index.d.ts +1 -0
  111. package/lib/cjs/types/adapter-express/studio/index.d.ts +1 -0
  112. package/lib/cjs/types/adapter-express/studio/studio-integration.d.ts +170 -0
  113. package/lib/cjs/types/index.d.ts +1 -1
  114. package/lib/esm/adapter-express/application-express.base.js +24 -0
  115. package/lib/esm/adapter-express/application-express.js +1656 -0
  116. package/lib/esm/adapter-express/application-express.types.js +1 -0
  117. package/lib/esm/adapter-express/express-utils/base-middleware.js +19 -0
  118. package/lib/esm/adapter-express/express-utils/conditional-middleware.js +96 -0
  119. package/lib/esm/adapter-express/express-utils/constants.js +63 -0
  120. package/lib/esm/adapter-express/express-utils/content/httpContent.js +6 -0
  121. package/lib/esm/adapter-express/express-utils/content-negotiation-decorators.js +120 -0
  122. package/lib/esm/adapter-express/express-utils/decorators.js +604 -0
  123. package/lib/esm/adapter-express/express-utils/exception-filter-decorators.js +6 -0
  124. package/lib/esm/adapter-express/express-utils/guard-context-factory.js +83 -0
  125. package/lib/esm/adapter-express/express-utils/guard-middleware.js +115 -0
  126. package/lib/esm/adapter-express/express-utils/guard-utils.js +14 -0
  127. package/lib/esm/adapter-express/express-utils/http-context-store.js +10 -0
  128. package/lib/esm/adapter-express/express-utils/http-status-middleware.js +116 -0
  129. package/lib/esm/adapter-express/express-utils/httpResponseMessage.js +29 -0
  130. package/lib/esm/adapter-express/express-utils/index.js +24 -0
  131. package/lib/esm/adapter-express/express-utils/interceptor-middleware.js +130 -0
  132. package/lib/esm/adapter-express/express-utils/interfaces.js +1 -0
  133. package/lib/esm/adapter-express/express-utils/inversify-express-server.js +1047 -0
  134. package/lib/esm/adapter-express/express-utils/lazy-module-middleware.js +236 -0
  135. package/lib/esm/adapter-express/express-utils/middleware-composition.js +89 -0
  136. package/lib/esm/adapter-express/express-utils/path-pattern-compat.js +125 -0
  137. package/lib/esm/adapter-express/express-utils/permission-preloader.middleware.js +45 -0
  138. package/lib/esm/adapter-express/express-utils/resolver-multer.js +30 -0
  139. package/lib/esm/adapter-express/express-utils/route-constraints.js +100 -0
  140. package/lib/esm/adapter-express/express-utils/scope-extractor.interface.js +1 -0
  141. package/lib/esm/adapter-express/express-utils/scope-extractor.js +63 -0
  142. package/lib/esm/adapter-express/express-utils/setup-authorization.js +68 -0
  143. package/lib/esm/adapter-express/express-utils/setup-event-system.js +110 -0
  144. package/lib/esm/adapter-express/express-utils/setup-interceptors.js +100 -0
  145. package/lib/esm/adapter-express/express-utils/setup-lazy-loading.js +225 -0
  146. package/lib/esm/adapter-express/express-utils/utils.js +68 -0
  147. package/lib/esm/adapter-express/express-utils/validation-decorators.js +199 -0
  148. package/lib/esm/adapter-express/express-utils/validation-service.js +251 -0
  149. package/lib/esm/adapter-express/index.js +7 -0
  150. package/lib/esm/adapter-express/micro-api/application-express-micro-container.js +48 -0
  151. package/lib/esm/adapter-express/micro-api/application-express-micro-route.js +128 -0
  152. package/lib/esm/adapter-express/micro-api/application-express-micro.js +157 -0
  153. package/lib/esm/adapter-express/micro-api/gateway/circuit-breaker.js +174 -0
  154. package/lib/esm/adapter-express/micro-api/gateway/index.js +5 -0
  155. package/lib/esm/adapter-express/micro-api/gateway/service-proxy.js +210 -0
  156. package/lib/esm/adapter-express/micro-api/index.js +10 -0
  157. package/lib/esm/adapter-express/micro-api/micro.js +266 -0
  158. package/lib/esm/adapter-express/micro-api/queue/index.js +4 -0
  159. package/lib/esm/adapter-express/micro-api/queue/queue.interface.js +1 -0
  160. package/lib/esm/adapter-express/micro-api/queue/rabbitmq-consumer.js +229 -0
  161. package/lib/esm/adapter-express/micro-api/serverless/aws-lambda.adapter.js +180 -0
  162. package/lib/esm/adapter-express/micro-api/serverless/cloudflare.adapter.js +155 -0
  163. package/lib/esm/adapter-express/micro-api/serverless/index.js +6 -0
  164. package/lib/esm/adapter-express/micro-api/serverless/vercel.adapter.js +99 -0
  165. package/lib/esm/adapter-express/micro-api/service-mesh/index.js +5 -0
  166. package/lib/esm/adapter-express/micro-api/service-mesh/service-client.js +191 -0
  167. package/lib/esm/adapter-express/micro-api/service-mesh/service-discovery.js +259 -0
  168. package/lib/esm/adapter-express/middleware/index.js +5 -0
  169. package/lib/esm/adapter-express/middleware/request-logging.middleware.js +239 -0
  170. package/lib/esm/adapter-express/render/constants.js +37 -0
  171. package/lib/esm/adapter-express/render/engine.js +51 -0
  172. package/lib/esm/adapter-express/render/index.js +1 -0
  173. package/lib/esm/adapter-express/render/resolve-render.js +30 -0
  174. package/lib/esm/adapter-express/studio/index.js +1 -0
  175. package/lib/esm/adapter-express/studio/studio-integration.js +236 -0
  176. package/lib/esm/index.mjs +1 -0
  177. package/lib/esm/package.json +3 -0
  178. package/lib/esm/types/adapter-express/application-express.base.d.ts +77 -0
  179. package/lib/esm/types/adapter-express/application-express.d.ts +453 -0
  180. package/lib/esm/types/adapter-express/application-express.types.d.ts +23 -0
  181. package/lib/esm/types/adapter-express/express-utils/base-middleware.d.ts +8 -0
  182. package/lib/esm/types/adapter-express/express-utils/conditional-middleware.d.ts +97 -0
  183. package/lib/esm/types/adapter-express/express-utils/constants.d.ts +57 -0
  184. package/lib/esm/types/adapter-express/express-utils/content/httpContent.d.ts +6 -0
  185. package/lib/esm/types/adapter-express/express-utils/content-negotiation-decorators.d.ts +94 -0
  186. package/lib/esm/types/adapter-express/express-utils/decorators.d.ts +257 -0
  187. package/lib/esm/types/adapter-express/express-utils/exception-filter-decorators.d.ts +6 -0
  188. package/lib/esm/types/adapter-express/express-utils/guard-context-factory.d.ts +17 -0
  189. package/lib/esm/types/adapter-express/express-utils/guard-middleware.d.ts +22 -0
  190. package/lib/esm/types/adapter-express/express-utils/guard-utils.d.ts +11 -0
  191. package/lib/esm/types/adapter-express/express-utils/http-context-store.d.ts +20 -0
  192. package/lib/esm/types/adapter-express/express-utils/http-status-middleware.d.ts +26 -0
  193. package/lib/esm/types/adapter-express/express-utils/httpResponseMessage.d.ts +14 -0
  194. package/lib/esm/types/adapter-express/express-utils/index.d.ts +30 -0
  195. package/lib/esm/types/adapter-express/express-utils/interceptor-middleware.d.ts +40 -0
  196. package/lib/esm/types/adapter-express/express-utils/interfaces.d.ts +115 -0
  197. package/lib/esm/types/adapter-express/express-utils/inversify-express-server.d.ts +172 -0
  198. package/lib/esm/types/adapter-express/express-utils/lazy-module-middleware.d.ts +122 -0
  199. package/lib/esm/types/adapter-express/express-utils/middleware-composition.d.ts +85 -0
  200. package/lib/esm/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
  201. package/lib/esm/types/adapter-express/express-utils/permission-preloader.middleware.d.ts +10 -0
  202. package/lib/esm/types/adapter-express/express-utils/resolver-multer.d.ts +7 -0
  203. package/lib/esm/types/adapter-express/express-utils/route-constraints.d.ts +98 -0
  204. package/lib/esm/types/adapter-express/express-utils/scope-extractor.d.ts +21 -0
  205. package/lib/esm/types/adapter-express/express-utils/scope-extractor.interface.d.ts +12 -0
  206. package/lib/esm/types/adapter-express/express-utils/setup-authorization.d.ts +34 -0
  207. package/lib/esm/types/adapter-express/express-utils/setup-event-system.d.ts +118 -0
  208. package/lib/esm/types/adapter-express/express-utils/setup-interceptors.d.ts +115 -0
  209. package/lib/esm/types/adapter-express/express-utils/setup-lazy-loading.d.ts +123 -0
  210. package/lib/esm/types/adapter-express/express-utils/utils.d.ts +24 -0
  211. package/lib/esm/types/adapter-express/express-utils/validation-decorators.d.ts +145 -0
  212. package/lib/esm/types/adapter-express/express-utils/validation-service.d.ts +88 -0
  213. package/lib/esm/types/adapter-express/index.d.ts +7 -0
  214. package/lib/esm/types/adapter-express/micro-api/application-express-micro-container.d.ts +47 -0
  215. package/lib/esm/types/adapter-express/micro-api/application-express-micro-route.d.ts +104 -0
  216. package/lib/esm/types/adapter-express/micro-api/application-express-micro.d.ts +72 -0
  217. package/lib/esm/types/adapter-express/micro-api/gateway/circuit-breaker.d.ts +111 -0
  218. package/lib/esm/types/adapter-express/micro-api/gateway/index.d.ts +5 -0
  219. package/lib/esm/types/adapter-express/micro-api/gateway/service-proxy.d.ts +83 -0
  220. package/lib/esm/types/adapter-express/micro-api/index.d.ts +7 -0
  221. package/lib/esm/types/adapter-express/micro-api/micro.d.ts +83 -0
  222. package/lib/esm/types/adapter-express/micro-api/queue/index.d.ts +5 -0
  223. package/lib/esm/types/adapter-express/micro-api/queue/queue.interface.d.ts +60 -0
  224. package/lib/esm/types/adapter-express/micro-api/queue/rabbitmq-consumer.d.ts +86 -0
  225. package/lib/esm/types/adapter-express/micro-api/serverless/aws-lambda.adapter.d.ts +77 -0
  226. package/lib/esm/types/adapter-express/micro-api/serverless/cloudflare.adapter.d.ts +64 -0
  227. package/lib/esm/types/adapter-express/micro-api/serverless/index.d.ts +6 -0
  228. package/lib/esm/types/adapter-express/micro-api/serverless/vercel.adapter.d.ts +56 -0
  229. package/lib/esm/types/adapter-express/micro-api/service-mesh/index.d.ts +5 -0
  230. package/lib/esm/types/adapter-express/micro-api/service-mesh/service-client.d.ts +122 -0
  231. package/lib/esm/types/adapter-express/micro-api/service-mesh/service-discovery.d.ts +150 -0
  232. package/lib/esm/types/adapter-express/middleware/index.d.ts +5 -0
  233. package/lib/esm/types/adapter-express/middleware/request-logging.middleware.d.ts +65 -0
  234. package/lib/esm/types/adapter-express/render/constants.d.ts +26 -0
  235. package/lib/esm/types/adapter-express/render/engine.d.ts +20 -0
  236. package/lib/esm/types/adapter-express/render/index.d.ts +5 -0
  237. package/lib/esm/types/adapter-express/render/resolve-render.d.ts +7 -0
  238. package/lib/esm/types/adapter-express/studio/index.d.ts +1 -0
  239. package/lib/esm/types/adapter-express/studio/studio-integration.d.ts +170 -0
  240. package/lib/esm/types/index.d.ts +1 -0
  241. package/lib/package.json +170 -146
  242. package/package.json +170 -146
  243. package/lib/cjs/di/di.interfaces.js +0 -10
  244. package/lib/cjs/types/di/di.interfaces.d.ts +0 -289
@@ -0,0 +1,1656 @@
1
+ import express from "express";
2
+ import * as fs from "node:fs";
3
+ // Note: We use the global `process` object directly instead of importing it
4
+ // because signal handlers (SIGTERM, SIGINT, etc.) don't work correctly when
5
+ // process is imported as an ES module in CommonJS compiled code.
6
+ import { AppContainer, Console, INTERCEPTOR_METADATA_KEY, LifecycleRegistry, Logger, Middleware, ProviderManager, BannerGenerator, MetricsCollector, resolveBannerConfig, } from "@expressots/core";
7
+ /**
8
+ * Metadata key used by `@provide` (and friends) in `@expressots/core`'s
9
+ * binding-decorator module. The constant lives at an internal path
10
+ * (`di/binding-decorator/constants`) so we hardcode the string here —
11
+ * it's part of the framework's stable runtime contract and is what
12
+ * `MetricsCollector` reads for its own `providers` count.
13
+ */
14
+ const PROVIDE_METADATA_KEY = "inversify-binding-decorators:provide";
15
+ import { RenderEngine } from "@expressots/shared";
16
+ import { HttpStatusCodeMiddleware } from "./express-utils/http-status-middleware.js";
17
+ import { InversifyExpressServer } from "./express-utils/inversify-express-server.js";
18
+ import { setEngineEjs, setEngineHandlebars, setEnginePug } from "./render/engine.js";
19
+ import { getControllersFromMetadata, getControllersFromContainer, getControllerMethodMetadata, getControllerMetadata, } from "./express-utils/utils.js";
20
+ import { initializeStudio, stopStudio, isStudioEnabled, reportStudioRuntimeInfo, rescanStudioRoutes, } from "./studio/index.js";
21
+ /**
22
+ * The AppExpress class provides methods for configuring and running an Express application.
23
+ * @class AppExpress
24
+ * @implements {IWebServer} - Interface for the WebServer application implementation.
25
+ * @extends {ApplicationBase} - Base class for the application implementation that provides lifecycle hooks.
26
+ * @method configure - Configures the InversifyJS container.
27
+ * @method listen - Start listening on the given port and environment.
28
+ * @method setGlobalRoutePrefix - Sets the global route prefix for the application.
29
+ * @method setEngine - Configures the application's view engine based on the provided configuration options.
30
+ * @method isDevelopment - Verifies if the current environment is development.
31
+ */
32
+ export class AppExpress {
33
+ logger = new Logger();
34
+ console = new Console();
35
+ app;
36
+ serverInstance = null;
37
+ port;
38
+ environment;
39
+ appContainer;
40
+ globalPrefix = "/";
41
+ middlewareManager;
42
+ middlewares = [];
43
+ providerManager;
44
+ renderOptions = {};
45
+ lifecycleRegistry;
46
+ isShuttingDown = false;
47
+ bannerGenerator = null;
48
+ bannerConfig;
49
+ shutdownHandlers = new Map();
50
+ studioConfig = {};
51
+ /**
52
+ * Latest snapshot of application metrics produced by `MetricsCollector`
53
+ * during banner display. We cache it so that `reportStudioRuntimeInfo`
54
+ * can forward the *runtime* provider/interceptor counts (which match
55
+ * what the CLI banner shows) to the Studio Agent without recomputing.
56
+ */
57
+ lastApplicationMetrics = null;
58
+ /** Track active connections for force-close during shutdown */
59
+ activeConnections = new Set();
60
+ /** Timeout for force-closing connections during shutdown (ms) */
61
+ shutdownTimeout = 5000;
62
+ /** Number of retries when port is in use (for hot-reload scenarios) */
63
+ portRetryAttempts = 10;
64
+ /** Delay between port retry attempts (ms) */
65
+ portRetryDelay = 500;
66
+ // Log buffering for banner-first display.
67
+ //
68
+ // Buffering is **opt-in** and is activated either by:
69
+ // 1. Constructing an `AppExpress` instance (the constructor calls
70
+ // `startLogBuffering()`), or
71
+ // 2. The framework calling `AppExpress.startLogBuffering()` explicitly
72
+ // from `bootstrap()` so logs emitted during container/module setup
73
+ // are captured before the AppExpress instance even exists.
74
+ //
75
+ // Importing `@expressots/adapter-express` does NOT touch stdio. Test
76
+ // harnesses, type-only consumers, and tooling that imports the module
77
+ // without ever booting an app will see normal `process.stdout` /
78
+ // `console.*` behavior. `micro()` calls `disableBuffering()` on entry
79
+ // because it does not use the banner system.
80
+ static originalStdoutWrite = null;
81
+ static originalStderrWrite = null;
82
+ static logBuffer = [];
83
+ static isBuffering = false;
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ static originalGlobalConsole = null;
86
+ /**
87
+ * Disable log buffering. Called by micro() to restore normal console output
88
+ * since micro API doesn't use the banner system.
89
+ * @public API
90
+ */
91
+ static disableBuffering() {
92
+ AppExpress.stopBuffering();
93
+ // Clear any buffered logs since micro() doesn't need them
94
+ AppExpress.logBuffer = [];
95
+ }
96
+ /**
97
+ * Start buffering all console output for the banner-first display flow.
98
+ * Captures both `console.*` and direct `process.stdout.write` / `process.stderr.write`
99
+ * calls so they can be flushed in the correct order after the banner displays.
100
+ *
101
+ * Idempotent: calling this multiple times is safe.
102
+ *
103
+ * @public API — called by `bootstrap()` so logs emitted during container
104
+ * setup are captured before the `AppExpress` instance exists. Also called
105
+ * automatically inside the constructor as a safety net.
106
+ */
107
+ static startLogBuffering() {
108
+ if (AppExpress.isBuffering)
109
+ return;
110
+ // Store original streams
111
+ AppExpress.originalStdoutWrite = process.stdout.write.bind(process.stdout);
112
+ AppExpress.originalStderrWrite = process.stderr.write.bind(process.stderr);
113
+ // Create wrapper functions that use fs.writeSync directly (always works
114
+ // in both CJS and ESM scope - hence the static `node:fs` import above).
115
+ const createOriginalConsoleMethod = (useStderr = false) => (...args) => {
116
+ const message = args
117
+ .map((a) => {
118
+ if (typeof a === "object" && a !== null) {
119
+ try {
120
+ return JSON.stringify(a, null, 2);
121
+ }
122
+ catch {
123
+ return String(a);
124
+ }
125
+ }
126
+ return String(a);
127
+ })
128
+ .join(" ") + "\n";
129
+ // Use fs.writeSync directly - this always works
130
+ fs.writeSync(useStderr ? 2 : 1, message);
131
+ };
132
+ AppExpress.originalGlobalConsole = {
133
+ log: createOriginalConsoleMethod(false),
134
+ info: createOriginalConsoleMethod(false),
135
+ warn: createOriginalConsoleMethod(true),
136
+ error: createOriginalConsoleMethod(true),
137
+ debug: createOriginalConsoleMethod(false),
138
+ };
139
+ AppExpress.logBuffer = [];
140
+ AppExpress.isBuffering = true;
141
+ // Create buffering functions for console methods
142
+ const bufferConsoleMethod = () => (...args) => {
143
+ const message = args
144
+ .map((a) => (typeof a === "object" && a !== null ? JSON.stringify(a) : String(a)))
145
+ .join(" ") + "\n";
146
+ AppExpress.logBuffer.push(message);
147
+ };
148
+ // Override console methods directly (not replacing the console object)
149
+ // This ensures even cached references to console.log will use the buffered version
150
+ console.log = bufferConsoleMethod();
151
+ console.info = bufferConsoleMethod();
152
+ console.warn = bufferConsoleMethod();
153
+ console.error = bufferConsoleMethod();
154
+ console.debug = bufferConsoleMethod();
155
+ // Also override process.stdout.write for direct writes (like our Logger)
156
+ const bufferWrite = (chunk) => {
157
+ const str = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
158
+ AppExpress.logBuffer.push(str);
159
+ return true;
160
+ };
161
+ // Use direct assignment for overriding
162
+ process.stdout.write = bufferWrite;
163
+ process.stderr.write = bufferWrite;
164
+ }
165
+ /**
166
+ * Stop buffering but keep the buffered logs for later flushing.
167
+ * This restores normal console/stdout output.
168
+ * @private
169
+ */
170
+ static stopBuffering() {
171
+ if (!AppExpress.isBuffering)
172
+ return;
173
+ // Restore original console methods using our wrapper functions
174
+ if (AppExpress.originalGlobalConsole) {
175
+ console.log = AppExpress.originalGlobalConsole.log;
176
+ console.info = AppExpress.originalGlobalConsole.info;
177
+ console.warn = AppExpress.originalGlobalConsole.warn;
178
+ console.error = AppExpress.originalGlobalConsole.error;
179
+ console.debug = AppExpress.originalGlobalConsole.debug;
180
+ }
181
+ // Restore original stdout/stderr by direct assignment
182
+ // (Object.defineProperty may not work correctly for stream.write)
183
+ if (AppExpress.originalStdoutWrite) {
184
+ process.stdout.write =
185
+ AppExpress.originalStdoutWrite;
186
+ }
187
+ if (AppExpress.originalStderrWrite) {
188
+ process.stderr.write =
189
+ AppExpress.originalStderrWrite;
190
+ }
191
+ AppExpress.isBuffering = false;
192
+ }
193
+ /**
194
+ * Flush all buffered logs to stdout.
195
+ * Should be called after stopBuffering() and after displaying the banner.
196
+ * @private
197
+ */
198
+ static flushBufferedLogs() {
199
+ const logs = AppExpress.logBuffer;
200
+ AppExpress.logBuffer = [];
201
+ for (const log of logs) {
202
+ if (AppExpress.originalStdoutWrite) {
203
+ AppExpress.originalStdoutWrite.call(process.stdout, log);
204
+ }
205
+ else {
206
+ process.stdout.write(log);
207
+ }
208
+ }
209
+ }
210
+ constructor() {
211
+ // Activate banner-first log buffering on first AppExpress construction.
212
+ // Idempotent — bootstrap() typically called this earlier so logs emitted
213
+ // during container/module setup were already buffered. micro() never
214
+ // reaches this constructor; it explicitly disables buffering itself.
215
+ AppExpress.startLogBuffering();
216
+ this.globalConfiguration();
217
+ }
218
+ /**
219
+ * Helper function to handle both sync and async method calls.
220
+ * If the result is a Promise, awaits it; otherwise returns immediately.
221
+ * @private
222
+ */
223
+ async handleSyncOrAsync(result) {
224
+ if (result instanceof Promise) {
225
+ return await result;
226
+ }
227
+ }
228
+ /**
229
+ * Implement this method to set up global configurations for the server.
230
+ * This method is called synchronously in the constructor before any other
231
+ * server initialization methods. Use this method to configure global settings
232
+ * that apply to the entire server application.
233
+ *
234
+ * Note: This method is synchronous and called during object construction.
235
+ * For asynchronous initialization, use `configureServices()` instead.
236
+ *
237
+ * @abstract
238
+ * @returns {void}
239
+ * @public API
240
+ */
241
+ globalConfiguration() { }
242
+ /**
243
+ * Implement this method to set up required services or configurations before
244
+ * the server starts. This is essential for initializing dependencies or settings
245
+ * necessary for server operation. Supports both synchronous and asynchronous setup.
246
+ *
247
+ * @abstract
248
+ * @returns {void | Promise<void>}
249
+ * @public API
250
+ */
251
+ configureServices() { }
252
+ /**
253
+ * Implement this method to execute actions or configurations after the server
254
+ * has started. Use this for operations that need to run once the server is
255
+ * operational. Supports both synchronous and asynchronous execution.
256
+ *
257
+ * @abstract
258
+ * @returns {void | Promise<void>}
259
+ * @public API
260
+ */
261
+ postServerInitialization() { }
262
+ /**
263
+ * Implement this method to handle cleanup and final actions when the server
264
+ * is shutting down. Ideal for closing resources, stopping tasks, or other
265
+ * cleanup procedures to ensure a graceful server shutdown. Supports both
266
+ * synchronous and asynchronous cleanup.
267
+ *
268
+ * The signal parameter indicates what triggered the shutdown:
269
+ * - SIGTERM: Graceful termination (e.g., Kubernetes pod shutdown)
270
+ * - SIGINT: User interrupt (e.g., Ctrl+C)
271
+ * - SIGHUP: Terminal hangup
272
+ * - SIGQUIT: Quit with core dump
273
+ * - SIGBREAK: Windows break signal
274
+ *
275
+ * @abstract
276
+ * @param signal - The signal that triggered the shutdown (optional for backward compatibility)
277
+ * @returns {void | Promise<void>}
278
+ * @public API
279
+ */
280
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
281
+ serverShutdown(signal) { }
282
+ /**
283
+ * Performs graceful shutdown of the application.
284
+ *
285
+ * Shutdown sequence:
286
+ * 1. Execute lifecycle shutdown hooks on all IShutdown providers
287
+ * 2. Call user's serverShutdown hook
288
+ * 3. Close the HTTP server to stop accepting new connections
289
+ *
290
+ * @param signal - The signal that triggered the shutdown
291
+ * @returns Promise that resolves when shutdown is complete
292
+ * @internal
293
+ */
294
+ async handleExit(signal) {
295
+ // Helper: race any drain against a hard cap. We never want a
296
+ // misbehaving cleanup hook (OTel exporter, slow DB driver, user
297
+ // shutdown promise that never resolves) to hold the whole exit
298
+ // chain. Each phase gets its own bound — total fits inside the
299
+ // outer `setShutdownTimeout` watchdog.
300
+ const withTimeout = async (p, ms) => {
301
+ const value = Promise.resolve(p).then(() => { });
302
+ const timer = new Promise((resolve) => setTimeout(resolve, ms).unref());
303
+ await Promise.race([value, timer]);
304
+ };
305
+ // 1. Stop Studio Agent if running. We cap this at 1.5s — the agent
306
+ // itself caps its websocket drain at 500ms and OpenTelemetry at
307
+ // 500ms, but auto-instrumentations can leave handles around so
308
+ // the wrapper close still occasionally drags. 1.5s is plenty.
309
+ await withTimeout(stopStudio(), 1500);
310
+ // 2. Execute lifecycle shutdown hooks on all IShutdown providers.
311
+ // Capped at the user-configured shutdown timeout (default 5s).
312
+ if (this.lifecycleRegistry) {
313
+ await withTimeout(this.lifecycleRegistry.executeShutdown(signal), this.shutdownTimeout);
314
+ }
315
+ // 3. Call user's serverShutdown hook (also capped).
316
+ await withTimeout(this.handleSyncOrAsync(this.serverShutdown(signal)), this.shutdownTimeout);
317
+ // 4. Gracefully close the HTTP server with aggressive connection
318
+ // teardown. Order matters: we destroy *all* tracked connections
319
+ // immediately (not just idle ones) before calling `close()`,
320
+ // because otherwise an active keep-alive request would hold the
321
+ // `close` callback open until either its keep-alive timer
322
+ // expires (~5s) or the inner `forceCloseTimeout` fires. Killing
323
+ // connections up-front lets `close` resolve in the next tick.
324
+ if (this.serverInstance) {
325
+ await new Promise((resolve) => {
326
+ const forceCloseTimeout = setTimeout(() => {
327
+ console.log(`⚠️ Force-closing ${this.activeConnections.size} active connections after ${this.shutdownTimeout}ms timeout`);
328
+ this.destroyAllConnections();
329
+ resolve();
330
+ }, this.shutdownTimeout);
331
+ forceCloseTimeout.unref();
332
+ this.serverInstance.close((err) => {
333
+ clearTimeout(forceCloseTimeout);
334
+ if (err) {
335
+ // Don't fail on close error during shutdown - just log it
336
+ console.log(`Note: Server close returned: ${err.message}`);
337
+ }
338
+ resolve();
339
+ });
340
+ // Aggressively kill keep-alive sockets so `close` actually
341
+ // resolves promptly. `closeAllConnections` is Node 18.2+; older
342
+ // versions silently no-op via the optional-call.
343
+ try {
344
+ this.serverInstance.closeAllConnections?.();
345
+ }
346
+ catch {
347
+ // best-effort — destroy our tracked set as a fallback
348
+ }
349
+ this.destroyAllConnections();
350
+ });
351
+ }
352
+ }
353
+ /**
354
+ * Destroy all active connections immediately.
355
+ * Used during forced shutdown.
356
+ * @private
357
+ */
358
+ destroyAllConnections() {
359
+ for (const socket of this.activeConnections) {
360
+ try {
361
+ socket.destroy();
362
+ }
363
+ catch {
364
+ // Ignore errors during connection destruction
365
+ }
366
+ }
367
+ this.activeConnections.clear();
368
+ }
369
+ /**
370
+ * Track a new connection for shutdown management.
371
+ * @private
372
+ */
373
+ trackConnection(socket) {
374
+ this.activeConnections.add(socket);
375
+ socket.once("close", () => {
376
+ this.activeConnections.delete(socket);
377
+ });
378
+ }
379
+ /**
380
+ * Initialize the InversifyJS container with the provided modules and options.
381
+ * @param appModules - An array of application modules to be loaded into the container.
382
+ * @param containerOptions - Container global configuration options.
383
+ * @option skipBaseClassChecks - Skip the base class checks for the container.
384
+ * @option autoBindInjectable - Automatically bind the injectable classes.
385
+ * @option defaultScope - The default scope to use for bindings.
386
+ *
387
+ * @returns The configured AppContainer instance.
388
+ * @public API
389
+ */
390
+ configContainer(appModules, containerOptions) {
391
+ this.appContainer = new AppContainer(containerOptions ? containerOptions : {});
392
+ if (!appModules) {
393
+ this.logger.error("No modules provided for container configuration", "adapter-express");
394
+ return;
395
+ }
396
+ this.appContainer.create(appModules);
397
+ this.providerManager = new ProviderManager(this.appContainer.Container);
398
+ const baseMiddleware = new Middleware();
399
+ // Create a wrapper that automatically injects container for exception filters
400
+ this.middlewareManager = this.createMiddlewareWrapper(baseMiddleware);
401
+ // Initialize lifecycle registry and discover providers implementing IBootstrap/IShutdown
402
+ this.lifecycleRegistry = new LifecycleRegistry(this.appContainer.Container);
403
+ this.lifecycleRegistry.discover();
404
+ return this.appContainer;
405
+ }
406
+ /**
407
+ * Creates a middleware wrapper that automatically injects container when exception filters are enabled
408
+ * This allows users to simply set enableExceptionFilters: true without manually passing the container
409
+ */
410
+ createMiddlewareWrapper(baseMiddleware) {
411
+ const container = this.appContainer?.Container;
412
+ // Create a proxy that intercepts setErrorHandler calls
413
+ return new Proxy(baseMiddleware, {
414
+ get(target, prop) {
415
+ if (prop === "setErrorHandler") {
416
+ return function (options) {
417
+ // Automatically inject container if enableExceptionFilters is true and container is available
418
+ const enhancedOptions = {
419
+ ...options,
420
+ container: options?.enableExceptionFilters && container ? container : options?.container,
421
+ };
422
+ target.setErrorHandler(enhancedOptions);
423
+ };
424
+ }
425
+ // Forward all other property access to the base middleware
426
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
427
+ const value = target[prop];
428
+ return typeof value === "function" ? value.bind(target) : value;
429
+ },
430
+ });
431
+ }
432
+ /**
433
+ * Get the ProviderManager instance.
434
+ * @returns The ProviderManager instance.
435
+ * @public API
436
+ */
437
+ get Provider() {
438
+ return this.providerManager;
439
+ }
440
+ /**
441
+ * Get the Middleware instance.
442
+ * @returns The Middleware instance.
443
+ * @public API
444
+ */
445
+ get Middleware() {
446
+ return this.middlewareManager;
447
+ }
448
+ /**
449
+ * Configures the Express application with the provided middleware entries.
450
+ * @param app - The Express application instance.
451
+ * @param middlewareEntries - An array of Express middleware entries to be applied.
452
+ */
453
+ async configureMiddleware(app, middlewareEntries) {
454
+ for (const entry of middlewareEntries) {
455
+ if (typeof entry === "function") {
456
+ app.use(entry);
457
+ // eslint-disable-next-line no-prototype-builtins
458
+ }
459
+ else if (entry?.hasOwnProperty("path")) {
460
+ const { path, middlewares } = entry;
461
+ const pathGlobal = this.globalPrefix + path;
462
+ for (const mid of middlewares) {
463
+ if (path) {
464
+ if (typeof mid === "function") {
465
+ app.use(pathGlobal, mid);
466
+ }
467
+ else {
468
+ const middleware = mid;
469
+ // Check if it's a BaseMiddleware instance (has handler method, not use)
470
+ const middlewareRecord = middleware;
471
+ if (middlewareRecord.handler && typeof middlewareRecord.handler === "function") {
472
+ // BaseMiddleware instance - wrap handler method
473
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
474
+ const baseMiddleware = middleware;
475
+ app.use(pathGlobal, (req, res, next) => {
476
+ baseMiddleware.handler(req, res, next);
477
+ });
478
+ }
479
+ else if (middleware.use) {
480
+ middleware.use = middleware.use.bind(middleware);
481
+ app.use(pathGlobal, middleware.use);
482
+ }
483
+ else {
484
+ this.logger.warn(`Middleware ${middleware.constructor?.name || "unknown"} does not have a 'use' or 'handler' method`, "application-express");
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ else {
491
+ const middleware = entry;
492
+ // Check if it's a BaseMiddleware instance (has handler method, not use)
493
+ // BaseMiddleware instances are handled specially in inversify-express-server
494
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
495
+ if (middleware.handler && typeof middleware.handler === "function") {
496
+ // BaseMiddleware instance - wrap handler method
497
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
498
+ const baseMiddleware = middleware;
499
+ app.use((req, res, next) => {
500
+ baseMiddleware.handler(req, res, next);
501
+ });
502
+ }
503
+ else if (middleware.use) {
504
+ middleware.use = middleware.use.bind(middleware);
505
+ app.use(middleware.use);
506
+ }
507
+ else {
508
+ this.logger.warn(`Middleware ${middleware.constructor?.name || "unknown"} does not have a 'use' or 'handler' method`, "application-express");
509
+ }
510
+ }
511
+ }
512
+ }
513
+ /**
514
+ * Create and configure the Express application.
515
+ * @param container - The InversifyJS container.
516
+ * @param middlewares - An array of Express middlewares to be applied.
517
+ * @returns The configured Application instance.
518
+ */
519
+ async init() {
520
+ if (!this.appContainer) {
521
+ this.logger.error("No container provided for application configuration", "adapter-express");
522
+ process.exit(1);
523
+ }
524
+ // Create Express app early so it's available during configureServices for render()
525
+ const tempApp = express();
526
+ this.Middleware.setExpressApp(tempApp);
527
+ // Initialize Studio Agent if available (adds middleware before user
528
+ // middlewares). At this point we already know the port the user asked
529
+ // us to listen on (set in `listen()` before `init()` is invoked) and
530
+ // the configured global prefix, so forward both — the agent uses them
531
+ // to populate the Studio Status page.
532
+ await initializeStudio(tempApp, {
533
+ ...this.studioConfig,
534
+ appPort: this.port,
535
+ globalPrefix: this.globalPrefix,
536
+ }, this.appContainer);
537
+ await this.handleSyncOrAsync(this.configureServices());
538
+ const sortedMiddlewarePipeline = this.Middleware.getMiddlewarePipeline();
539
+ const pipeline = sortedMiddlewarePipeline.map((entry) => entry.middleware);
540
+ this.middlewares.push(...pipeline);
541
+ /* Apply the status code to the response */
542
+ this.middlewares.unshift(new HttpStatusCodeMiddleware(this.globalPrefix));
543
+ const expressServer = new InversifyExpressServer(this.appContainer.Container, null, { rootPath: this.globalPrefix }, tempApp);
544
+ // Pass ContentNegotiationService to InversifyExpressServer if available
545
+ const contentNegotiationService = this.Middleware.getContentNegotiationService();
546
+ if (contentNegotiationService) {
547
+ expressServer.setContentNegotiationService(contentNegotiationService);
548
+ }
549
+ // Pass ValidationService to InversifyExpressServer if validation is configured
550
+ const validationConfig = this.Middleware.getValidationConfig?.();
551
+ if (validationConfig) {
552
+ // `.js` extension required by NodeNext for ESM consumers; the
553
+ // CJS build accepts it unchanged.
554
+ const { ValidationService } = await import("./express-utils/validation-service.js");
555
+ const { ClassValidatorAdapter } = await import("@expressots/core");
556
+ const validationService = new ValidationService();
557
+ validationService.enable(validationConfig);
558
+ // Register ClassValidatorAdapter by default
559
+ const classValidatorAdapter = new ClassValidatorAdapter();
560
+ validationService.getRegistry().register(classValidatorAdapter);
561
+ // Register any additional adapters from config
562
+ if (validationConfig.adapters) {
563
+ for (const AdapterClass of validationConfig.adapters) {
564
+ const adapter = new AdapterClass();
565
+ validationService.getRegistry().register(adapter);
566
+ }
567
+ }
568
+ expressServer.setValidationService(validationService);
569
+ }
570
+ expressServer.setConfig((app) => {
571
+ this.configureMiddleware(app, this.middlewares);
572
+ });
573
+ expressServer.setErrorConfig((app) => {
574
+ if (this.Middleware.getErrorHandler()) {
575
+ app.use(this.Middleware.getErrorHandler());
576
+ }
577
+ });
578
+ this.app = expressServer.build();
579
+ return this;
580
+ }
581
+ /**
582
+ * Start listening on the given port and environment.
583
+ * @param port - The port number to listen on.
584
+ * @param appInfo - Optional message to display the app name and version.
585
+ * @public API
586
+ */
587
+ async listen(port, appInfo) {
588
+ // Capture wall-clock start so we can report total boot duration to the
589
+ // Studio Status page once `app.listen()` resolves with the actual port.
590
+ const listenStartedAt = Date.now();
591
+ // Close existing server instance if it exists
592
+ if (this.serverInstance) {
593
+ this.logger.warn("Closing existing server instance before starting new one", "adapter-express");
594
+ await this.closeExistingServer();
595
+ this.logger.info("✓ Application reloaded", "adapter-express");
596
+ }
597
+ // Remove old signal handlers to prevent duplicates
598
+ this.removeShutdownHandlers();
599
+ // Reset shutdown flag
600
+ this.isShuttingDown = false;
601
+ // Resolve banner configuration with environment-specific overrides
602
+ const resolvedBannerConfig = resolveBannerConfig(this.bannerConfig, this.environment || "development");
603
+ // Initialize banner generator with resolved config
604
+ this.bannerGenerator = new BannerGenerator(resolvedBannerConfig);
605
+ this.environment = this.environment || "development";
606
+ this.port = typeof port === "string" ? parseInt(port, 10) : port;
607
+ try {
608
+ await this.init();
609
+ await this.configEngine();
610
+ this.app.set("env", this.environment);
611
+ // Stop buffering and restore normal output (but don't flush yet)
612
+ AppExpress.stopBuffering();
613
+ // Flush all buffered logs that were captured during initialization
614
+ AppExpress.flushBufferedLogs();
615
+ }
616
+ catch (error) {
617
+ // Ensure buffering is stopped and logs are flushed even on error
618
+ AppExpress.stopBuffering();
619
+ AppExpress.flushBufferedLogs();
620
+ throw error;
621
+ }
622
+ // Ensure port is available (handles hot-reload scenarios)
623
+ // This will kill the previous process if needed - safest approach for dev experience
624
+ const portAvailable = await this.ensurePortAvailable(this.port);
625
+ if (!portAvailable) {
626
+ const errorMessage = `Port ${this.port} is still in use and could not be freed`;
627
+ this.logger.error(errorMessage, "adapter-express");
628
+ this.logger.info("💡 Try manually killing the process:", "adapter-express");
629
+ this.logger.info(process.platform === "win32"
630
+ ? ` netstat -ano | findstr :${this.port} && taskkill /F /PID <pid>`
631
+ : ` lsof -ti:${this.port} | xargs kill -9`, "adapter-express");
632
+ throw new Error(errorMessage);
633
+ }
634
+ return new Promise((resolve, reject) => {
635
+ this.serverInstance = this.app.listen(this.port, async () => {
636
+ // Track all connections for graceful shutdown
637
+ // This enables force-closing connections during hot-reload
638
+ this.serverInstance.on("connection", (socket) => {
639
+ this.trackConnection(socket);
640
+ });
641
+ // Update port with actual assigned port (important for port 0 auto-assign)
642
+ this.port = this.serverInstance?.address()?.port || this.port;
643
+ // Display startup banner AFTER server starts (so we have the correct port)
644
+ this.displayStartupBanner(appInfo);
645
+ // Push live runtime details to the Studio Agent so the Status
646
+ // page swaps "—" for real values. We forward the same numbers
647
+ // `MetricsCollector` produced for the CLI banner (providers,
648
+ // interceptors, middleware) — they come from DI metadata at
649
+ // runtime and include framework-registered items that the
650
+ // agent's static file scan can't see. We also forward the
651
+ // *names* of those items so the Studio drill-down can list
652
+ // them. No-op when Studio is disabled or when the installed
653
+ // agent is too old to support it.
654
+ reportStudioRuntimeInfo({
655
+ appPort: this.port,
656
+ globalPrefix: this.globalPrefix,
657
+ startupMs: Date.now() - listenStartedAt,
658
+ providerCount: this.lastApplicationMetrics?.providers,
659
+ interceptorCount: this.lastApplicationMetrics?.interceptors,
660
+ middlewareCount: this.lastApplicationMetrics?.middleware,
661
+ runtimeItems: this.collectStudioRuntimeItems(),
662
+ middlewarePreset: this.collectMiddlewarePresetInfo(),
663
+ });
664
+ // Re-scan routes now that `InversifyExpressServer.build()` has
665
+ // populated the Express `_router` stack. The agent's first scan
666
+ // happens before controllers are bound (Studio middleware ships
667
+ // ahead of route registration so it can capture every request),
668
+ // so without this rescan newly-added or never-bound controllers
669
+ // never appear in the Studio Routes / Architecture views.
670
+ // Fire-and-forget; the Studio Agent broadcasts the result over WS.
671
+ void rescanStudioRoutes();
672
+ // Setup signal handlers for graceful shutdown
673
+ // Supported signals:
674
+ // - SIGTERM: Standard termination (Kubernetes, Docker, process managers)
675
+ // - SIGINT: User interrupt (Ctrl+C)
676
+ // - SIGHUP: Terminal hangup
677
+ // - SIGQUIT: Quit with core dump request
678
+ // - SIGBREAK: Windows break signal (Ctrl+Break)
679
+ // - SIGUSR2: Used by nodemon for restart (not on Windows)
680
+ const shutdownSignals = [
681
+ "SIGTERM",
682
+ "SIGINT",
683
+ "SIGHUP",
684
+ "SIGQUIT",
685
+ "SIGBREAK",
686
+ ...(process.platform !== "win32" ? ["SIGUSR2"] : []),
687
+ ];
688
+ for (const signal of shutdownSignals) {
689
+ // Skip if handler already registered (prevents duplicates)
690
+ if (this.shutdownHandlers.has(signal)) {
691
+ continue;
692
+ }
693
+ const handler = () => {
694
+ // Prevent multiple shutdown attempts
695
+ if (this.isShuttingDown) {
696
+ return;
697
+ }
698
+ this.isShuttingDown = true;
699
+ // Emit the shutdown notice through the framework Logger so it
700
+ // matches the standard "[ExpressoTS] … INFO [context] …" output.
701
+ // The leading newline keeps it off the terminal's "^C" echo line.
702
+ process.stdout.write("\n");
703
+ this.logger.info(`Signal ${signal} received, initiating graceful shutdown...`, "adapter-express");
704
+ // Hard overall cap on the graceful shutdown. `handleExit` chains
705
+ // several `await`s — Studio agent stop, lifecycle shutdown
706
+ // hooks, the user's `serverShutdown`, and finally
707
+ // `serverInstance.close`. If any of those hang (an unresponsive
708
+ // OpenTelemetry exporter, a pending DB transaction, a slow
709
+ // user hook, an HTTP keep-alive socket the OS hasn't reaped
710
+ // yet), the host process otherwise sits in
711
+ // "📡 Signal SIGINT received, initiating graceful shutdown…"
712
+ // for minutes. Capping the whole pipeline keeps the developer
713
+ // ergonomics tight (Ctrl+C is interactive — they want their
714
+ // prompt back now) while still allowing fast hooks to run to
715
+ // completion.
716
+ //
717
+ // We expose the cap so apps with legitimately long drains
718
+ // (e.g. flushing a 50k-message queue) can opt into a longer
719
+ // timeout via `setShutdownTimeout`.
720
+ const overallCap = this.shutdownTimeout + 3000;
721
+ let forced = false;
722
+ const overallTimer = setTimeout(() => {
723
+ forced = true;
724
+ console.warn(`⚠️ Graceful shutdown exceeded ${overallCap}ms; ` +
725
+ `force-exiting. If this happens routinely, raise ` +
726
+ `\`setShutdownTimeout\` or audit your IShutdown hooks.`);
727
+ this.destroyAllConnections();
728
+ process.exit(0);
729
+ }, overallCap);
730
+ // Don't let the watchdog timer keep the event loop alive on
731
+ // its own; if everything else releases the loop it's fine
732
+ // for `process.exit(0)` to fire from `handleExit` cleanly.
733
+ overallTimer.unref();
734
+ this.handleExit(signal)
735
+ .then(() => {
736
+ if (forced)
737
+ return;
738
+ clearTimeout(overallTimer);
739
+ this.logger.info("Graceful shutdown completed", "adapter-express");
740
+ // Flush stdout before exiting. Writing an empty chunk with a
741
+ // callback guarantees the log line above is fully drained to
742
+ // the terminal (the callback only fires after prior queued
743
+ // writes complete), so the message can't appear after the
744
+ // shell has already redrawn its prompt.
745
+ process.stdout.write("", () => {
746
+ process.exit(0);
747
+ });
748
+ })
749
+ .catch((error) => {
750
+ if (forced)
751
+ return;
752
+ clearTimeout(overallTimer);
753
+ this.logger.error(`Error during shutdown: ${error.message}`, "adapter-express");
754
+ process.stderr.write("", () => {
755
+ process.exit(1);
756
+ });
757
+ });
758
+ };
759
+ // Store handler for later removal and register it
760
+ this.shutdownHandlers.set(signal, handler);
761
+ process.on(signal, handler);
762
+ }
763
+ // Setup exit handler to force-close connections immediately
764
+ // This is a last-resort handler for when signals don't arrive or complete in time
765
+ // (e.g., during hot-reload when the process is killed quickly)
766
+ const exitHandler = () => {
767
+ if (this.serverInstance) {
768
+ // Synchronously destroy all connections - this is our last chance
769
+ this.destroyAllConnections();
770
+ // Try to close the server synchronously (won't block but releases the port faster)
771
+ try {
772
+ this.serverInstance.close();
773
+ }
774
+ catch {
775
+ // Ignore errors during exit
776
+ }
777
+ }
778
+ };
779
+ // Register exit handler (only once)
780
+ if (!this.shutdownHandlers.has("exit")) {
781
+ this.shutdownHandlers.set("exit", exitHandler);
782
+ process.once("exit", exitHandler);
783
+ }
784
+ try {
785
+ // Call user's postServerInitialization hook
786
+ await this.handleSyncOrAsync(this.postServerInitialization());
787
+ // Execute bootstrap lifecycle hooks on all IBootstrap providers
788
+ if (this.lifecycleRegistry) {
789
+ await this.lifecycleRegistry.executeBootstrap();
790
+ }
791
+ resolve(this);
792
+ }
793
+ catch (error) {
794
+ this.logger.error(`Error during post-server initialization: ${error}`, "adapter-express");
795
+ reject(error);
796
+ }
797
+ });
798
+ this.serverInstance?.on("error", (error) => {
799
+ // Handle EADDRINUSE error with helpful suggestions
800
+ if (error.code === "EADDRINUSE") {
801
+ const port = this.port;
802
+ const errorMessage = `Port ${port} is already in use`;
803
+ const suggestions = [
804
+ `Try a different port: Set PORT environment variable to another value`,
805
+ `Find and stop the process using port ${port}`,
806
+ process.platform === "win32"
807
+ ? `On Windows: netstat -ano | findstr :${port}`
808
+ : `On Linux/Mac: lsof -ti:${port} | xargs kill`,
809
+ ];
810
+ this.logger.error(errorMessage, "adapter-express");
811
+ this.logger.info("💡 Suggestions:", "adapter-express");
812
+ suggestions.forEach((suggestion) => {
813
+ this.logger.info(` • ${suggestion}`, "adapter-express");
814
+ });
815
+ reject(new Error(`${errorMessage}. ${suggestions[0]}`));
816
+ }
817
+ else {
818
+ this.logger.error(`Error starting server: ${error.message}`, "adapter-express");
819
+ reject(error);
820
+ }
821
+ });
822
+ });
823
+ }
824
+ /**
825
+ * Close existing server instance if it exists.
826
+ * @private
827
+ */
828
+ async closeExistingServer() {
829
+ if (this.serverInstance) {
830
+ return new Promise((resolve) => {
831
+ this.serverInstance.close(() => {
832
+ this.serverInstance = null;
833
+ resolve();
834
+ });
835
+ // Force close after timeout
836
+ setTimeout(() => {
837
+ if (this.serverInstance) {
838
+ this.serverInstance = null;
839
+ resolve();
840
+ }
841
+ }, 1000);
842
+ });
843
+ }
844
+ }
845
+ /**
846
+ * Wait for a specified duration.
847
+ * @private
848
+ */
849
+ delay(ms) {
850
+ return new Promise((resolve) => setTimeout(resolve, ms));
851
+ }
852
+ /**
853
+ * Kill the process using a specific port.
854
+ * @private
855
+ */
856
+ async killProcessOnPort(port) {
857
+ const { exec } = await import("child_process");
858
+ const { promisify } = await import("util");
859
+ const execAsync = promisify(exec);
860
+ try {
861
+ if (process.platform === "win32") {
862
+ // Windows: Find PID using netstat and kill it
863
+ const { stdout } = await execAsync(`netstat -ano | findstr :${port} | findstr LISTENING`);
864
+ const lines = stdout.trim().split("\n");
865
+ for (const line of lines) {
866
+ const parts = line.trim().split(/\s+/);
867
+ const pid = parts[parts.length - 1];
868
+ if (pid && pid !== String(process.pid) && /^\d+$/.test(pid)) {
869
+ try {
870
+ await execAsync(`taskkill /F /PID ${pid}`);
871
+ return true;
872
+ }
873
+ catch {
874
+ // Process might have already exited
875
+ }
876
+ }
877
+ }
878
+ }
879
+ else {
880
+ // Linux/Mac: Use lsof to find PID and kill it
881
+ try {
882
+ const { stdout } = await execAsync(`lsof -ti:${port}`);
883
+ const pids = stdout.trim().split("\n").filter(Boolean);
884
+ for (const pid of pids) {
885
+ if (pid !== String(process.pid)) {
886
+ try {
887
+ await execAsync(`kill -9 ${pid}`);
888
+ return true;
889
+ }
890
+ catch {
891
+ // Process might have already exited
892
+ }
893
+ }
894
+ }
895
+ }
896
+ catch {
897
+ // No process found on port
898
+ }
899
+ }
900
+ }
901
+ catch {
902
+ // Command failed - port might already be free
903
+ }
904
+ return false;
905
+ }
906
+ /**
907
+ * Check if the port is available by attempting to bind to it.
908
+ * @private
909
+ */
910
+ async isPortAvailable(port) {
911
+ const net = await import("net");
912
+ return new Promise((resolve) => {
913
+ const testServer = net.createServer();
914
+ testServer.once("error", () => {
915
+ resolve(false);
916
+ });
917
+ testServer.once("listening", () => {
918
+ testServer.close(() => {
919
+ resolve(true);
920
+ });
921
+ });
922
+ testServer.listen(port);
923
+ });
924
+ }
925
+ /**
926
+ * Ensure the port is available, killing the existing process if needed.
927
+ * This is the safest approach for hot-reload scenarios.
928
+ * @private
929
+ */
930
+ async ensurePortAvailable(port) {
931
+ // First, check if port is already available
932
+ if (await this.isPortAvailable(port)) {
933
+ return true;
934
+ }
935
+ // Try to kill the process on the port
936
+ let killed = await this.killProcessOnPort(port);
937
+ if (killed) {
938
+ // Wait a moment for the port to be released
939
+ await this.delay(500);
940
+ }
941
+ // Retry multiple times to check if port is now available
942
+ // Hot reload scenarios may need more time for the old process to shut down
943
+ for (let attempt = 1; attempt <= this.portRetryAttempts; attempt++) {
944
+ if (await this.isPortAvailable(port)) {
945
+ return true;
946
+ }
947
+ // Try to kill again if still not available (process might be slow to release)
948
+ if (attempt % 3 === 0) {
949
+ killed = await this.killProcessOnPort(port);
950
+ if (killed) {
951
+ await this.delay(300);
952
+ }
953
+ }
954
+ if (attempt < this.portRetryAttempts) {
955
+ await this.delay(this.portRetryDelay);
956
+ }
957
+ }
958
+ return false;
959
+ }
960
+ /**
961
+ * Remove existing shutdown signal handlers to prevent duplicates.
962
+ * @private
963
+ */
964
+ removeShutdownHandlers() {
965
+ this.shutdownHandlers.forEach((handler, signal) => {
966
+ // Handle "exit" event specially (it's not a signal but we track it the same way)
967
+ if (signal === "exit") {
968
+ process.removeListener("exit", handler);
969
+ }
970
+ else {
971
+ process.removeListener(signal, handler);
972
+ }
973
+ });
974
+ this.shutdownHandlers.clear();
975
+ // Also clear any tracked connections from previous runs
976
+ this.activeConnections.clear();
977
+ }
978
+ /**
979
+ * Sets the global route prefix for the application.
980
+ * @method setGlobalRoutePrefix
981
+ * @param {string} prefix - The prefix to use for all routes.
982
+ * @public API
983
+ */
984
+ async setGlobalRoutePrefix(prefix) {
985
+ this.globalPrefix = prefix;
986
+ }
987
+ /**
988
+ * Configures the application's view engine based on the provided configuration options.
989
+ */
990
+ async configEngine() {
991
+ if (this.renderOptions.engine) {
992
+ switch (this.renderOptions.engine) {
993
+ case RenderEngine.Engine.HBS:
994
+ await setEngineHandlebars(this.app, this.renderOptions.options);
995
+ break;
996
+ case RenderEngine.Engine.EJS:
997
+ await setEngineEjs(this.app, this.renderOptions.options);
998
+ break;
999
+ case RenderEngine.Engine.PUG:
1000
+ await setEnginePug(this.app, this.renderOptions.options);
1001
+ break;
1002
+ default:
1003
+ throw new Error("Unsupported engine type!");
1004
+ }
1005
+ }
1006
+ }
1007
+ /**
1008
+ * Configures the application's view engine based on the provided configuration options.
1009
+ * @method setEngine
1010
+ * @template T - A generic type extending from RenderTemplateOptions.
1011
+ *
1012
+ * @param {Engine} engine - The view engine to set
1013
+ * @param {EngineOptions} [options] - The configuration options for the view engine
1014
+ * @public API
1015
+ */
1016
+ /**
1017
+ * Configure the startup banner display.
1018
+ * Can be called in configureServices() or globalConfiguration().
1019
+ *
1020
+ * @param config - Banner configuration options
1021
+ * @example
1022
+ * ```typescript
1023
+ * export class App extends AppExpress {
1024
+ * configureServices(): void {
1025
+ * this.setBanner({
1026
+ * style: "full",
1027
+ * showMetrics: true,
1028
+ * showFeatures: true,
1029
+ * showConfig: true,
1030
+ * showPerformance: true,
1031
+ * showResources: true,
1032
+ * // Environment-specific overrides
1033
+ * environment: {
1034
+ * production: {
1035
+ * style: "compact",
1036
+ * showConfig: false,
1037
+ * showResources: false,
1038
+ * },
1039
+ * },
1040
+ * });
1041
+ * }
1042
+ * }
1043
+ * ```
1044
+ * @public API
1045
+ */
1046
+ setBanner(config) {
1047
+ this.bannerConfig = config;
1048
+ }
1049
+ /**
1050
+ * Configure ExpressoTS Studio integration.
1051
+ * When enabled and @expressots/studio-agent is installed, automatically
1052
+ * instruments the application for request recording and real-time monitoring.
1053
+ *
1054
+ * By default, Studio is auto-enabled in development if the package is installed.
1055
+ * Use this method to customize behavior or enable in production.
1056
+ *
1057
+ * @param config - Studio configuration options
1058
+ * @example
1059
+ * ```typescript
1060
+ * export class App extends AppExpress {
1061
+ * configureServices(): void {
1062
+ * this.setStudio({
1063
+ * enabled: true, // Force enable (default: auto in dev)
1064
+ * port: 3334, // WebSocket port for UI connection
1065
+ * serviceName: 'my-app', // Service name for tracing
1066
+ * });
1067
+ * }
1068
+ * }
1069
+ * ```
1070
+ * @public API
1071
+ */
1072
+ setStudio(config) {
1073
+ this.studioConfig = config;
1074
+ }
1075
+ /**
1076
+ * Check if ExpressoTS Studio is currently enabled.
1077
+ * @returns Boolean indicating if Studio Agent is running.
1078
+ * @public API
1079
+ */
1080
+ isStudioEnabled() {
1081
+ return isStudioEnabled();
1082
+ }
1083
+ /**
1084
+ * Configure a view engine for server-side rendering.
1085
+ *
1086
+ * @deprecated Use `this.Middleware.render()` instead. Will be removed in v5.0.0.
1087
+ *
1088
+ * @example Migration
1089
+ * ```typescript
1090
+ * // Before (deprecated)
1091
+ * this.setEngine(RenderEngine.Engine.EJS, { viewsDir: 'views' });
1092
+ *
1093
+ * // After (recommended)
1094
+ * this.Middleware.render({ engine: 'ejs', viewsDir: 'views' });
1095
+ *
1096
+ * // Or with auto-detection
1097
+ * this.Middleware.render();
1098
+ * ```
1099
+ *
1100
+ * @param engine - The view engine to set
1101
+ * @param options - The configuration options for the view engine
1102
+ * @public API
1103
+ */
1104
+ async setEngine(engine, options) {
1105
+ this.logger.warn("setEngine() is deprecated. Use this.Middleware.render() instead. Will be removed in v5.0.0.", "adapter-express");
1106
+ try {
1107
+ // Bridge to new render system
1108
+ const engineMap = {
1109
+ ejs: "ejs",
1110
+ pug: "pug",
1111
+ hbs: "hbs",
1112
+ };
1113
+ const engineName = engineMap[engine] || engine;
1114
+ // Try to use the new render system
1115
+ await this.Middleware.render({
1116
+ engine: engineName,
1117
+ viewsDir: options?.viewsDir,
1118
+ partialsDir: options?.partialsDir,
1119
+ });
1120
+ }
1121
+ catch {
1122
+ // Fallback to old system if new system fails
1123
+ if (options) {
1124
+ this.renderOptions = { engine, options };
1125
+ }
1126
+ else {
1127
+ this.renderOptions = { engine };
1128
+ }
1129
+ }
1130
+ }
1131
+ /**
1132
+ * Verifies if the current environment is development.
1133
+ * @returns A boolean value indicating whether the current environment is development or not.
1134
+ * @public API
1135
+ */
1136
+ async isDevelopment() {
1137
+ // Check Express app environment first (most reliable)
1138
+ if (this.app) {
1139
+ return this.app.get("env") === "development";
1140
+ }
1141
+ // Fallback to this.environment (set by bootstrap())
1142
+ if (this.environment) {
1143
+ return this.environment === "development";
1144
+ }
1145
+ // Fallback to process.env.NODE_ENV
1146
+ if (process.env.NODE_ENV) {
1147
+ return process.env.NODE_ENV === "development";
1148
+ }
1149
+ // Default to false if nothing is set
1150
+ return false;
1151
+ }
1152
+ /**
1153
+ * Get the underlying HTTP server. (default: Express.js)
1154
+ * @returns The underlying HTTP server after initialization.
1155
+ * @public API
1156
+ */
1157
+ async getHttpServer() {
1158
+ if (!this.serverInstance) {
1159
+ this.logger.error("Server instance not initialized yet", "adapter-express");
1160
+ throw new Error("Server instance not initialized yet");
1161
+ }
1162
+ return Promise.resolve(this.serverInstance);
1163
+ }
1164
+ /**
1165
+ * Get the port the server is listening on.
1166
+ * Useful for dynamic port assignment (port: 0) in testing scenarios.
1167
+ * @returns The actual port number the server is bound to.
1168
+ * @public API
1169
+ */
1170
+ async getPort() {
1171
+ if (!this.serverInstance) {
1172
+ this.logger.error("Server instance not initialized yet", "adapter-express");
1173
+ throw new Error("Server instance not initialized yet");
1174
+ }
1175
+ const address = this.serverInstance.address();
1176
+ if (address && typeof address === "object" && "port" in address) {
1177
+ return Promise.resolve(address.port);
1178
+ }
1179
+ throw new Error("Unable to determine server port");
1180
+ }
1181
+ /**
1182
+ * Detect API versions from @Version() decorators on controllers.
1183
+ * @returns Array of unique API versions (e.g., ["v1", "v2"])
1184
+ * @private
1185
+ */
1186
+ detectApiVersions() {
1187
+ try {
1188
+ const controllers = getControllersFromMetadata();
1189
+ const versions = new Set();
1190
+ controllers.forEach((controllerTarget) => {
1191
+ // Cast DecoratorTarget to NewableFunction for metadata access
1192
+ const controllerConstructor = controllerTarget;
1193
+ // Check controller-level version
1194
+ const controllerMetadata = getControllerMetadata(controllerConstructor);
1195
+ if (controllerMetadata?.version) {
1196
+ const version = String(controllerMetadata.version);
1197
+ // Normalize version format (ensure "v" prefix)
1198
+ const normalizedVersion = version.startsWith("v") ? version : `v${version}`;
1199
+ versions.add(normalizedVersion);
1200
+ }
1201
+ // Check method-level versions
1202
+ const methodMetadata = getControllerMethodMetadata(controllerConstructor);
1203
+ if (methodMetadata) {
1204
+ methodMetadata.forEach((method) => {
1205
+ if (method.version) {
1206
+ const version = String(method.version);
1207
+ const normalizedVersion = version.startsWith("v") ? version : `v${version}`;
1208
+ versions.add(normalizedVersion);
1209
+ }
1210
+ });
1211
+ }
1212
+ });
1213
+ return Array.from(versions).sort();
1214
+ }
1215
+ catch (error) {
1216
+ // If metadata not available, return empty array
1217
+ return [];
1218
+ }
1219
+ }
1220
+ /**
1221
+ * Harvest provider + interceptor *names* from DI metadata so the
1222
+ * Studio Status page can drill down into the items behind the
1223
+ * "Providers" / "Interceptors" counters.
1224
+ *
1225
+ * Reads the same metadata that {@link MetricsCollector} uses for the
1226
+ * CLI banner — so what shows up here is the source of truth, not the
1227
+ * agent's static file scan (which can't see framework-registered
1228
+ * items like `Logger` or `LifecycleRegistry`).
1229
+ *
1230
+ * Returns `undefined` instead of an empty object so {@link
1231
+ * reportStudioRuntimeInfo} can skip forwarding when nothing was
1232
+ * harvested (keeps the WS payload small).
1233
+ */
1234
+ collectStudioRuntimeItems() {
1235
+ try {
1236
+ const providerMetadata = Reflect.getMetadata(PROVIDE_METADATA_KEY, Reflect) || [];
1237
+ const providers = [];
1238
+ for (const entry of providerMetadata) {
1239
+ const name = entry?.implementationType?.name;
1240
+ if (typeof name === "string" && name.length > 0) {
1241
+ providers.push({ name, source: "provide" });
1242
+ }
1243
+ }
1244
+ const interceptorMetadata = Reflect.getMetadata(INTERCEPTOR_METADATA_KEY.interceptor, Reflect) || [];
1245
+ const interceptors = [];
1246
+ for (const entry of interceptorMetadata) {
1247
+ const name = entry?.interceptor?.name;
1248
+ if (typeof name === "string" && name.length > 0) {
1249
+ interceptors.push({
1250
+ name,
1251
+ priority: entry?.priority,
1252
+ source: "metadata",
1253
+ });
1254
+ }
1255
+ }
1256
+ const middleware = this.collectMiddlewarePipelineItems();
1257
+ const middlewareBindings = this.collectMiddlewareBindings();
1258
+ if (providers.length === 0 &&
1259
+ interceptors.length === 0 &&
1260
+ !middleware &&
1261
+ !middlewareBindings) {
1262
+ return undefined;
1263
+ }
1264
+ return { providers, interceptors, middleware, middlewareBindings };
1265
+ }
1266
+ catch {
1267
+ return undefined;
1268
+ }
1269
+ }
1270
+ /**
1271
+ * Harvest controller- and route-scoped middleware bindings from
1272
+ * Reflect metadata. Each entry describes a single edge the Studio
1273
+ * architecture map should draw, e.g. "AuthMiddleware → UserController
1274
+ * (route POST /users/:id)".
1275
+ *
1276
+ * The middleware values stored on `ControllerMetadata.middleware` are
1277
+ * a polymorphic union (class, function, registered name, conditional
1278
+ * config, …). We normalise each to a display name; entries we can't
1279
+ * name (anonymous arrow functions, plain object configs without a
1280
+ * `name` field) are omitted. The agent's static scan picks up the
1281
+ * remaining named cases via decorator parsing — between the two
1282
+ * sources Studio sees a complete graph for the common patterns.
1283
+ */
1284
+ collectMiddlewareBindings() {
1285
+ try {
1286
+ const controllers = getControllersFromMetadata();
1287
+ if (!controllers || controllers.length === 0)
1288
+ return undefined;
1289
+ const out = [];
1290
+ const nameOf = (value) => {
1291
+ if (value == null)
1292
+ return null;
1293
+ if (typeof value === "string")
1294
+ return value;
1295
+ if (typeof value === "symbol") {
1296
+ const desc = value.description;
1297
+ return desc && desc.length > 0 ? desc : null;
1298
+ }
1299
+ if (typeof value === "function") {
1300
+ const fnName = value.name;
1301
+ return typeof fnName === "string" && fnName.length > 0 ? fnName : null;
1302
+ }
1303
+ if (typeof value === "object") {
1304
+ const candidate = value.name;
1305
+ if (typeof candidate === "string" && candidate.length > 0) {
1306
+ return candidate;
1307
+ }
1308
+ const ctorName = value.constructor?.name;
1309
+ if (typeof ctorName === "string" && ctorName.length > 0 && ctorName !== "Object") {
1310
+ return ctorName;
1311
+ }
1312
+ }
1313
+ return null;
1314
+ };
1315
+ for (const controllerTarget of controllers) {
1316
+ const controllerCtor = controllerTarget;
1317
+ const controllerName = controllerCtor.name;
1318
+ if (typeof controllerName !== "string" || controllerName.length === 0) {
1319
+ continue;
1320
+ }
1321
+ const ctrlMeta = getControllerMetadata(controllerCtor);
1322
+ if (ctrlMeta?.middleware && Array.isArray(ctrlMeta.middleware)) {
1323
+ for (const mw of ctrlMeta.middleware) {
1324
+ const middlewareName = nameOf(mw);
1325
+ if (!middlewareName)
1326
+ continue;
1327
+ out.push({
1328
+ middlewareName,
1329
+ scope: "controller",
1330
+ controllerName,
1331
+ });
1332
+ }
1333
+ }
1334
+ const methodMeta = getControllerMethodMetadata(controllerCtor);
1335
+ if (Array.isArray(methodMeta)) {
1336
+ const basePath = ctrlMeta?.path ?? "";
1337
+ for (const route of methodMeta) {
1338
+ if (!route?.middleware || !Array.isArray(route.middleware))
1339
+ continue;
1340
+ const httpMethod = typeof route.method === "string" ? route.method.toUpperCase() : undefined;
1341
+ const fullPath = this.joinRoutePath(basePath, route.path);
1342
+ for (const mw of route.middleware) {
1343
+ const middlewareName = nameOf(mw);
1344
+ if (!middlewareName)
1345
+ continue;
1346
+ out.push({
1347
+ middlewareName,
1348
+ scope: "route",
1349
+ controllerName,
1350
+ controllerMethod: typeof route.key === "string" ? route.key : undefined,
1351
+ httpMethod,
1352
+ routePath: fullPath,
1353
+ });
1354
+ }
1355
+ }
1356
+ }
1357
+ }
1358
+ return out.length > 0 ? out : undefined;
1359
+ }
1360
+ catch {
1361
+ return undefined;
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Combine a controller's base path with a route path, normalising
1366
+ * leading/trailing slashes. Mirrors the simpler logic Studio uses to
1367
+ * build `RouteInfo.path` so the bindings line up with route entries.
1368
+ */
1369
+ joinRoutePath(basePath, routePath) {
1370
+ const base = basePath?.startsWith("/") ? basePath : `/${basePath ?? ""}`;
1371
+ if (!routePath || routePath === "/" || routePath === "")
1372
+ return base || "/";
1373
+ const tail = routePath.startsWith("/") ? routePath : `/${routePath}`;
1374
+ return (base + tail).replace(/\/+/g, "/");
1375
+ }
1376
+ /**
1377
+ * Collect the ordered middleware pipeline from the Middleware service
1378
+ * for forwarding to Studio. Uses feature-detection so older core
1379
+ * versions that lack `getPipelineInfo()` won't break.
1380
+ */
1381
+ collectMiddlewarePipelineItems() {
1382
+ try {
1383
+ const mw = this.Middleware;
1384
+ const getPipelineInfo = mw.getPipelineInfo;
1385
+ if (typeof getPipelineInfo !== "function")
1386
+ return undefined;
1387
+ const info = getPipelineInfo.call(mw);
1388
+ if (!info || !info.entries || info.entries.length === 0)
1389
+ return undefined;
1390
+ return info.entries.map((e) => ({
1391
+ name: e.name,
1392
+ category: e.category,
1393
+ type: e.type,
1394
+ order: e.order,
1395
+ path: e.path !== "Global" ? e.path : undefined,
1396
+ }));
1397
+ }
1398
+ catch {
1399
+ return undefined;
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Build the middleware preset info snapshot for Studio. Reads the
1404
+ * last applied preset from the Middleware service and transforms it
1405
+ * into the shape Studio expects.
1406
+ */
1407
+ collectMiddlewarePresetInfo() {
1408
+ try {
1409
+ const mw = this.Middleware;
1410
+ const getPreset = mw.getLastAppliedPreset;
1411
+ if (typeof getPreset !== "function")
1412
+ return undefined;
1413
+ const preset = getPreset.call(mw);
1414
+ if (!preset)
1415
+ return undefined;
1416
+ const cfg = preset.config;
1417
+ const parse = cfg.parse && typeof cfg.parse === "object"
1418
+ ? {
1419
+ json: cfg.parse.json && typeof cfg.parse.json === "object"
1420
+ ? { limit: cfg.parse.json.limit }
1421
+ : undefined,
1422
+ urlencoded: cfg.parse.urlencoded && typeof cfg.parse.urlencoded === "object"
1423
+ ? {
1424
+ limit: cfg.parse.urlencoded.limit,
1425
+ extended: cfg.parse.urlencoded.extended,
1426
+ }
1427
+ : undefined,
1428
+ cookies: !!cfg.parse.cookies,
1429
+ }
1430
+ : cfg.parse
1431
+ ? { json: { limit: "100kb" }, cookies: false }
1432
+ : undefined;
1433
+ let security;
1434
+ if (typeof cfg.security === "string") {
1435
+ security = resolveSecurityTierForStudio(cfg.security);
1436
+ }
1437
+ else if (cfg.security && typeof cfg.security === "object") {
1438
+ const sec = cfg.security;
1439
+ security = {
1440
+ helmet: sec.headers !== false,
1441
+ cors: sec.cors && typeof sec.cors === "object"
1442
+ ? sec.cors
1443
+ : sec.cors !== false
1444
+ ? { origin: true }
1445
+ : undefined,
1446
+ rateLimit: sec.rateLimit && typeof sec.rateLimit === "object"
1447
+ ? sec.rateLimit
1448
+ : sec.rateLimit
1449
+ ? { windowMs: 60000, max: 100 }
1450
+ : false,
1451
+ };
1452
+ }
1453
+ const compress = cfg.compress
1454
+ ? {
1455
+ enabled: true,
1456
+ level: typeof cfg.compress === "object" ? cfg.compress.level : undefined,
1457
+ }
1458
+ : { enabled: false };
1459
+ const logger = cfg.logger
1460
+ ? {
1461
+ enabled: true,
1462
+ implementation: typeof cfg.logger === "object" ? cfg.logger.implementation : "auto",
1463
+ }
1464
+ : { enabled: false };
1465
+ return {
1466
+ name: preset.name,
1467
+ hasOverrides: preset.hasOverrides,
1468
+ parse,
1469
+ security,
1470
+ compress,
1471
+ logger,
1472
+ };
1473
+ }
1474
+ catch {
1475
+ return undefined;
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Display middleware startup logs after the banner.
1480
+ *
1481
+ * Warnings (e.g. missing optional packages like `helmet`) are always surfaced
1482
+ * so the developer can act on them. Informational entries (e.g. "Security
1483
+ * configured", "Applied preset: api") are demoted to `debug` since the
1484
+ * dashboard already shows the active middleware count; set `LOG_LEVEL=DEBUG`
1485
+ * to see the full breakdown.
1486
+ * @private
1487
+ */
1488
+ displayMiddlewareStartupLogs() {
1489
+ const isDev = this.environment === "development";
1490
+ if (!isDev)
1491
+ return;
1492
+ const startupLogs = this.Middleware.getStartupLogs();
1493
+ if (startupLogs.length === 0)
1494
+ return;
1495
+ startupLogs.forEach((log) => {
1496
+ if (log.type === "warn") {
1497
+ this.logger.warn(log.message, "middleware");
1498
+ }
1499
+ else {
1500
+ this.logger.withContext("middleware").debug(log.message);
1501
+ }
1502
+ });
1503
+ this.Middleware.clearStartupLogs();
1504
+ }
1505
+ /**
1506
+ * Display startup banner with application metrics.
1507
+ * @param appInfo - Application info
1508
+ * @private
1509
+ */
1510
+ displayStartupBanner(appInfo) {
1511
+ if (!this.bannerGenerator) {
1512
+ // Fallback to old console message if banner generator not initialized
1513
+ this.console.messageServer(this.port, this.environment || "development", appInfo);
1514
+ // Log CI detection after banner, before middleware logs
1515
+ this.displayCIDetectionLogs(appInfo);
1516
+ // Still display middleware startup logs even in fallback mode
1517
+ this.displayMiddlewareStartupLogs();
1518
+ return;
1519
+ }
1520
+ try {
1521
+ let finalAppInfo = appInfo;
1522
+ if (!finalAppInfo?.apiVersions || finalAppInfo.apiVersions.length === 0) {
1523
+ const apiVersions = this.detectApiVersions();
1524
+ if (apiVersions.length > 0) {
1525
+ finalAppInfo = {
1526
+ ...appInfo,
1527
+ appName: appInfo?.appName || "App",
1528
+ appVersion: appInfo?.appVersion || "not provided",
1529
+ apiVersions,
1530
+ };
1531
+ }
1532
+ }
1533
+ // Detect API versions from controllers
1534
+ const detectedApiVersions = finalAppInfo?.apiVersions || [];
1535
+ // Collect metrics. Cache the result on the instance so the Studio
1536
+ // integration can forward the *runtime* counts (providers /
1537
+ // interceptors / middleware) to the agent — those values come from
1538
+ // DI metadata and registries, which our static file scanner can't
1539
+ // see, so without this the Studio Status page would disagree with
1540
+ // the CLI banner.
1541
+ const { metrics, features } = MetricsCollector.collect(this.appContainer.Container, {
1542
+ getControllersFromMetadata: () => getControllersFromMetadata(),
1543
+ getControllersFromContainer: () => getControllersFromContainer(this.appContainer.Container, false),
1544
+ getControllerMethodMetadata: (constructor) => getControllerMethodMetadata(constructor),
1545
+ getMiddlewareCount: () => this.Middleware.getMiddlewarePipeline().length,
1546
+ hasContentNegotiation: () => !!this.Middleware.getContentNegotiationService(),
1547
+ hasSmartValidation: () => !!this.Middleware.getValidationConfig(),
1548
+ hasAuthorization: () => this.appContainer.Container.isBound("IGuardCache"),
1549
+ hasExceptionFilters: () => !!this.Middleware.getErrorHandler(),
1550
+ hasApiVersioning: () => detectedApiVersions.length > 0,
1551
+ hasGlobalRoutePrefix: () => !!this.globalPrefix && this.globalPrefix !== "/",
1552
+ hasErrorHandler: () => !!this.Middleware.getErrorHandler(),
1553
+ hasRequestLogging: () => {
1554
+ // Check if any request logging middleware is in the pipeline
1555
+ const pipeline = this.Middleware.getPipelineInfo();
1556
+ return pipeline.entries.some((e) => e.category === "logging" || e.name.toLowerCase().includes("logging"));
1557
+ },
1558
+ });
1559
+ // Persist the metrics so `reportStudioRuntimeInfo` (called from the
1560
+ // listen callback) can forward live provider/interceptor counts.
1561
+ this.lastApplicationMetrics = metrics;
1562
+ // Discover providers for introspection
1563
+ this.Provider.discover();
1564
+ // Get middleware and provider views for banner
1565
+ const middlewareView = this.Middleware.getFormattedView();
1566
+ const providerView = this.Provider.getFormattedView();
1567
+ // Prepare banner data with extended info
1568
+ const bannerData = {
1569
+ appInfo: finalAppInfo,
1570
+ metrics,
1571
+ features,
1572
+ middlewareView,
1573
+ providerView,
1574
+ };
1575
+ // Display banner
1576
+ this.bannerGenerator.display(this.port, this.environment || "development", finalAppInfo, metrics, features, {
1577
+ Prefix: this.globalPrefix || "/",
1578
+ "Node Version": process.version,
1579
+ Platform: process.platform,
1580
+ }, bannerData);
1581
+ // Log CI detection after banner, before middleware logs
1582
+ this.displayCIDetectionLogs(appInfo);
1583
+ // Automatically display middleware startup logs after banner (transparent to user)
1584
+ this.displayMiddlewareStartupLogs();
1585
+ }
1586
+ catch (error) {
1587
+ // Fallback to old console message on error
1588
+ this.logger.warn("Failed to display startup banner, using fallback", "adapter-express", error);
1589
+ this.console.messageServer(this.port, this.environment || "development", appInfo);
1590
+ // Log CI detection after banner, before middleware logs
1591
+ this.displayCIDetectionLogs(appInfo);
1592
+ // Still display middleware startup logs even in fallback mode
1593
+ this.displayMiddlewareStartupLogs();
1594
+ }
1595
+ }
1596
+ /**
1597
+ * Display CI detection logs after the banner but before middleware logs.
1598
+ * @param appInfo - Application info containing CI detection data
1599
+ * @private
1600
+ */
1601
+ displayCIDetectionLogs(appInfo) {
1602
+ if (appInfo?.ciDetection?.detected) {
1603
+ this.logger.info(`🔍 CI environment detected: ${appInfo.ciDetection.platform}`, "bootstrap");
1604
+ this.logger.info(`✅ Skipping .env file loading (using process.env)`, "bootstrap");
1605
+ }
1606
+ }
1607
+ }
1608
+ /**
1609
+ * Resolve a named security tier string into the display-friendly shape
1610
+ * expected by the Studio Middleware card. Mirrors the defaults applied
1611
+ * by `Middleware.getSecurityPreset()` in `@expressots/core`.
1612
+ */
1613
+ function resolveSecurityTierForStudio(tier) {
1614
+ switch (tier) {
1615
+ case "api":
1616
+ return {
1617
+ tier,
1618
+ helmet: true,
1619
+ cors: {
1620
+ origin: true,
1621
+ credentials: true,
1622
+ methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
1623
+ allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
1624
+ },
1625
+ rateLimit: { windowMs: 60000, max: 100 },
1626
+ };
1627
+ case "strict":
1628
+ return {
1629
+ tier,
1630
+ helmet: true,
1631
+ cors: { origin: false },
1632
+ rateLimit: { windowMs: 60000, max: 50 },
1633
+ };
1634
+ case "relaxed":
1635
+ return {
1636
+ tier,
1637
+ helmet: true,
1638
+ cors: { origin: true },
1639
+ rateLimit: false,
1640
+ };
1641
+ case "minimal":
1642
+ return {
1643
+ tier,
1644
+ helmet: false,
1645
+ rateLimit: false,
1646
+ };
1647
+ case "standard":
1648
+ default:
1649
+ return {
1650
+ tier,
1651
+ helmet: true,
1652
+ cors: { origin: true },
1653
+ rateLimit: false,
1654
+ };
1655
+ }
1656
+ }