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