@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.
- package/LICENSE.md +21 -21
- package/README.md +61 -118
- package/lib/CHANGELOG.md +36 -5
- package/lib/README.md +61 -118
- package/lib/cjs/adapter-express/application-express.base.js +3 -1
- package/lib/cjs/adapter-express/application-express.js +1405 -85
- package/lib/cjs/adapter-express/express-utils/conditional-middleware.js +102 -0
- package/lib/cjs/adapter-express/express-utils/constants.js +17 -0
- package/lib/cjs/adapter-express/express-utils/content-negotiation-decorators.js +129 -0
- package/lib/cjs/adapter-express/express-utils/decorators.js +225 -59
- package/lib/cjs/adapter-express/express-utils/exception-filter-decorators.js +11 -0
- package/lib/cjs/adapter-express/express-utils/guard-context-factory.js +84 -0
- package/lib/cjs/adapter-express/express-utils/guard-middleware.js +115 -0
- package/lib/cjs/adapter-express/express-utils/guard-utils.js +18 -0
- package/lib/cjs/adapter-express/express-utils/http-context-store.js +15 -0
- package/lib/cjs/adapter-express/express-utils/http-status-middleware.js +37 -2
- package/lib/cjs/adapter-express/express-utils/index.js +67 -1
- package/lib/cjs/adapter-express/express-utils/interceptor-middleware.js +132 -0
- package/lib/cjs/adapter-express/express-utils/inversify-express-server.js +827 -64
- package/lib/cjs/adapter-express/express-utils/lazy-module-middleware.js +241 -0
- package/lib/cjs/adapter-express/express-utils/middleware-composition.js +95 -0
- package/lib/cjs/adapter-express/express-utils/path-pattern-compat.js +129 -0
- package/lib/cjs/adapter-express/express-utils/permission-preloader.middleware.js +48 -0
- package/lib/cjs/adapter-express/express-utils/route-constraints.js +104 -0
- package/lib/cjs/adapter-express/express-utils/scope-extractor.interface.js +2 -0
- package/lib/cjs/adapter-express/express-utils/scope-extractor.js +66 -0
- package/lib/cjs/adapter-express/express-utils/setup-authorization.js +71 -0
- package/lib/cjs/adapter-express/express-utils/setup-event-system.js +113 -0
- package/lib/cjs/adapter-express/express-utils/setup-interceptors.js +103 -0
- package/lib/cjs/adapter-express/express-utils/setup-lazy-loading.js +228 -0
- package/lib/cjs/adapter-express/express-utils/utils.js +30 -12
- package/lib/cjs/adapter-express/express-utils/validation-decorators.js +205 -0
- package/lib/cjs/adapter-express/express-utils/validation-service.js +252 -0
- package/lib/cjs/adapter-express/index.js +7 -5
- package/lib/cjs/adapter-express/micro-api/application-express-micro-route.js +31 -1
- package/lib/cjs/adapter-express/micro-api/application-express-micro.js +8 -38
- package/lib/cjs/adapter-express/micro-api/gateway/circuit-breaker.js +174 -0
- package/lib/cjs/adapter-express/micro-api/gateway/index.js +11 -0
- package/lib/cjs/adapter-express/micro-api/gateway/service-proxy.js +214 -0
- package/lib/cjs/adapter-express/micro-api/index.js +27 -3
- package/lib/cjs/adapter-express/micro-api/micro.js +272 -0
- package/lib/cjs/adapter-express/micro-api/queue/index.js +8 -0
- package/lib/cjs/adapter-express/micro-api/queue/queue.interface.js +2 -0
- package/lib/cjs/adapter-express/micro-api/queue/rabbitmq-consumer.js +255 -0
- package/lib/cjs/adapter-express/micro-api/serverless/aws-lambda.adapter.js +183 -0
- package/lib/cjs/adapter-express/micro-api/serverless/cloudflare.adapter.js +158 -0
- package/lib/cjs/adapter-express/micro-api/serverless/index.js +12 -0
- package/lib/cjs/adapter-express/micro-api/serverless/vercel.adapter.js +102 -0
- package/lib/cjs/adapter-express/micro-api/service-mesh/index.js +10 -0
- package/lib/cjs/adapter-express/micro-api/service-mesh/service-client.js +194 -0
- package/lib/cjs/adapter-express/micro-api/service-mesh/service-discovery.js +261 -0
- package/lib/cjs/adapter-express/middleware/index.js +21 -0
- package/lib/cjs/adapter-express/middleware/request-logging.middleware.js +244 -0
- package/lib/cjs/adapter-express/render/engine.js +15 -15
- package/lib/cjs/adapter-express/render/index.js +5 -0
- package/lib/cjs/adapter-express/studio/index.js +10 -0
- package/lib/cjs/adapter-express/studio/studio-integration.js +267 -0
- package/lib/cjs/index.js +1 -1
- package/lib/cjs/types/adapter-express/application-express.base.d.ts +20 -7
- package/lib/cjs/types/adapter-express/application-express.d.ts +316 -33
- package/lib/cjs/types/adapter-express/express-utils/base-middleware.d.ts +2 -2
- package/lib/cjs/types/adapter-express/express-utils/conditional-middleware.d.ts +97 -0
- package/lib/cjs/types/adapter-express/express-utils/constants.d.ts +13 -0
- package/lib/cjs/types/adapter-express/express-utils/content-negotiation-decorators.d.ts +94 -0
- package/lib/cjs/types/adapter-express/express-utils/decorators.d.ts +54 -6
- package/lib/cjs/types/adapter-express/express-utils/exception-filter-decorators.d.ts +6 -0
- package/lib/cjs/types/adapter-express/express-utils/guard-context-factory.d.ts +17 -0
- package/lib/cjs/types/adapter-express/express-utils/guard-middleware.d.ts +22 -0
- package/lib/cjs/types/adapter-express/express-utils/guard-utils.d.ts +11 -0
- package/lib/cjs/types/adapter-express/express-utils/http-context-store.d.ts +20 -0
- package/lib/cjs/types/adapter-express/express-utils/httpResponseMessage.d.ts +1 -1
- package/lib/cjs/types/adapter-express/express-utils/index.d.ts +30 -2
- package/lib/cjs/types/adapter-express/express-utils/interceptor-middleware.d.ts +40 -0
- package/lib/cjs/types/adapter-express/express-utils/interfaces.d.ts +42 -5
- package/lib/cjs/types/adapter-express/express-utils/inversify-express-server.d.ts +114 -2
- package/lib/cjs/types/adapter-express/express-utils/lazy-module-middleware.d.ts +122 -0
- package/lib/cjs/types/adapter-express/express-utils/middleware-composition.d.ts +85 -0
- package/lib/cjs/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
- package/lib/cjs/types/adapter-express/express-utils/permission-preloader.middleware.d.ts +10 -0
- package/lib/cjs/types/adapter-express/express-utils/route-constraints.d.ts +98 -0
- package/lib/cjs/types/adapter-express/express-utils/scope-extractor.d.ts +21 -0
- package/lib/cjs/types/adapter-express/express-utils/scope-extractor.interface.d.ts +12 -0
- package/lib/cjs/types/adapter-express/express-utils/setup-authorization.d.ts +34 -0
- package/lib/cjs/types/adapter-express/express-utils/setup-event-system.d.ts +118 -0
- package/lib/cjs/types/adapter-express/express-utils/setup-interceptors.d.ts +115 -0
- package/lib/cjs/types/adapter-express/express-utils/setup-lazy-loading.d.ts +123 -0
- package/lib/cjs/types/adapter-express/express-utils/utils.d.ts +17 -2
- package/lib/cjs/types/adapter-express/express-utils/validation-decorators.d.ts +145 -0
- package/lib/cjs/types/adapter-express/express-utils/validation-service.d.ts +88 -0
- package/lib/cjs/types/adapter-express/index.d.ts +6 -4
- package/lib/cjs/types/adapter-express/micro-api/application-express-micro-route.d.ts +25 -14
- package/lib/cjs/types/adapter-express/micro-api/application-express-micro.d.ts +3 -10
- package/lib/cjs/types/adapter-express/micro-api/gateway/circuit-breaker.d.ts +111 -0
- package/lib/cjs/types/adapter-express/micro-api/gateway/index.d.ts +5 -0
- package/lib/cjs/types/adapter-express/micro-api/gateway/service-proxy.d.ts +83 -0
- package/lib/cjs/types/adapter-express/micro-api/index.d.ts +7 -1
- package/lib/cjs/types/adapter-express/micro-api/micro.d.ts +83 -0
- package/lib/cjs/types/adapter-express/micro-api/queue/index.d.ts +5 -0
- package/lib/cjs/types/adapter-express/micro-api/queue/queue.interface.d.ts +60 -0
- package/lib/cjs/types/adapter-express/micro-api/queue/rabbitmq-consumer.d.ts +86 -0
- package/lib/cjs/types/adapter-express/micro-api/serverless/aws-lambda.adapter.d.ts +77 -0
- package/lib/cjs/types/adapter-express/micro-api/serverless/cloudflare.adapter.d.ts +64 -0
- package/lib/cjs/types/adapter-express/micro-api/serverless/index.d.ts +6 -0
- package/lib/cjs/types/adapter-express/micro-api/serverless/vercel.adapter.d.ts +56 -0
- package/lib/cjs/types/adapter-express/micro-api/service-mesh/index.d.ts +5 -0
- package/lib/cjs/types/adapter-express/micro-api/service-mesh/service-client.d.ts +122 -0
- package/lib/cjs/types/adapter-express/micro-api/service-mesh/service-discovery.d.ts +150 -0
- package/lib/cjs/types/adapter-express/middleware/index.d.ts +5 -0
- package/lib/cjs/types/adapter-express/middleware/request-logging.middleware.d.ts +65 -0
- package/lib/cjs/types/adapter-express/render/index.d.ts +1 -0
- package/lib/cjs/types/adapter-express/studio/index.d.ts +1 -0
- package/lib/cjs/types/adapter-express/studio/studio-integration.d.ts +170 -0
- package/lib/cjs/types/index.d.ts +1 -1
- package/lib/esm/adapter-express/application-express.base.js +24 -0
- package/lib/esm/adapter-express/application-express.js +1656 -0
- package/lib/esm/adapter-express/application-express.types.js +1 -0
- package/lib/esm/adapter-express/express-utils/base-middleware.js +19 -0
- package/lib/esm/adapter-express/express-utils/conditional-middleware.js +96 -0
- package/lib/esm/adapter-express/express-utils/constants.js +63 -0
- package/lib/esm/adapter-express/express-utils/content/httpContent.js +6 -0
- package/lib/esm/adapter-express/express-utils/content-negotiation-decorators.js +120 -0
- package/lib/esm/adapter-express/express-utils/decorators.js +604 -0
- package/lib/esm/adapter-express/express-utils/exception-filter-decorators.js +6 -0
- package/lib/esm/adapter-express/express-utils/guard-context-factory.js +83 -0
- package/lib/esm/adapter-express/express-utils/guard-middleware.js +115 -0
- package/lib/esm/adapter-express/express-utils/guard-utils.js +14 -0
- package/lib/esm/adapter-express/express-utils/http-context-store.js +10 -0
- package/lib/esm/adapter-express/express-utils/http-status-middleware.js +116 -0
- package/lib/esm/adapter-express/express-utils/httpResponseMessage.js +29 -0
- package/lib/esm/adapter-express/express-utils/index.js +24 -0
- package/lib/esm/adapter-express/express-utils/interceptor-middleware.js +130 -0
- package/lib/esm/adapter-express/express-utils/interfaces.js +1 -0
- package/lib/esm/adapter-express/express-utils/inversify-express-server.js +1047 -0
- package/lib/esm/adapter-express/express-utils/lazy-module-middleware.js +236 -0
- package/lib/esm/adapter-express/express-utils/middleware-composition.js +89 -0
- package/lib/esm/adapter-express/express-utils/path-pattern-compat.js +125 -0
- package/lib/esm/adapter-express/express-utils/permission-preloader.middleware.js +45 -0
- package/lib/esm/adapter-express/express-utils/resolver-multer.js +30 -0
- package/lib/esm/adapter-express/express-utils/route-constraints.js +100 -0
- package/lib/esm/adapter-express/express-utils/scope-extractor.interface.js +1 -0
- package/lib/esm/adapter-express/express-utils/scope-extractor.js +63 -0
- package/lib/esm/adapter-express/express-utils/setup-authorization.js +68 -0
- package/lib/esm/adapter-express/express-utils/setup-event-system.js +110 -0
- package/lib/esm/adapter-express/express-utils/setup-interceptors.js +100 -0
- package/lib/esm/adapter-express/express-utils/setup-lazy-loading.js +225 -0
- package/lib/esm/adapter-express/express-utils/utils.js +68 -0
- package/lib/esm/adapter-express/express-utils/validation-decorators.js +199 -0
- package/lib/esm/adapter-express/express-utils/validation-service.js +251 -0
- package/lib/esm/adapter-express/index.js +7 -0
- package/lib/esm/adapter-express/micro-api/application-express-micro-container.js +48 -0
- package/lib/esm/adapter-express/micro-api/application-express-micro-route.js +128 -0
- package/lib/esm/adapter-express/micro-api/application-express-micro.js +157 -0
- package/lib/esm/adapter-express/micro-api/gateway/circuit-breaker.js +174 -0
- package/lib/esm/adapter-express/micro-api/gateway/index.js +5 -0
- package/lib/esm/adapter-express/micro-api/gateway/service-proxy.js +210 -0
- package/lib/esm/adapter-express/micro-api/index.js +10 -0
- package/lib/esm/adapter-express/micro-api/micro.js +266 -0
- package/lib/esm/adapter-express/micro-api/queue/index.js +4 -0
- package/lib/esm/adapter-express/micro-api/queue/queue.interface.js +1 -0
- package/lib/esm/adapter-express/micro-api/queue/rabbitmq-consumer.js +229 -0
- package/lib/esm/adapter-express/micro-api/serverless/aws-lambda.adapter.js +180 -0
- package/lib/esm/adapter-express/micro-api/serverless/cloudflare.adapter.js +155 -0
- package/lib/esm/adapter-express/micro-api/serverless/index.js +6 -0
- package/lib/esm/adapter-express/micro-api/serverless/vercel.adapter.js +99 -0
- package/lib/esm/adapter-express/micro-api/service-mesh/index.js +5 -0
- package/lib/esm/adapter-express/micro-api/service-mesh/service-client.js +191 -0
- package/lib/esm/adapter-express/micro-api/service-mesh/service-discovery.js +259 -0
- package/lib/esm/adapter-express/middleware/index.js +5 -0
- package/lib/esm/adapter-express/middleware/request-logging.middleware.js +239 -0
- package/lib/esm/adapter-express/render/constants.js +37 -0
- package/lib/esm/adapter-express/render/engine.js +51 -0
- package/lib/esm/adapter-express/render/index.js +1 -0
- package/lib/esm/adapter-express/render/resolve-render.js +30 -0
- package/lib/esm/adapter-express/studio/index.js +1 -0
- package/lib/esm/adapter-express/studio/studio-integration.js +236 -0
- package/lib/esm/index.mjs +1 -0
- package/lib/esm/package.json +3 -0
- package/lib/esm/types/adapter-express/application-express.base.d.ts +77 -0
- package/lib/esm/types/adapter-express/application-express.d.ts +453 -0
- package/lib/esm/types/adapter-express/application-express.types.d.ts +23 -0
- package/lib/esm/types/adapter-express/express-utils/base-middleware.d.ts +8 -0
- package/lib/esm/types/adapter-express/express-utils/conditional-middleware.d.ts +97 -0
- package/lib/esm/types/adapter-express/express-utils/constants.d.ts +57 -0
- package/lib/esm/types/adapter-express/express-utils/content/httpContent.d.ts +6 -0
- package/lib/esm/types/adapter-express/express-utils/content-negotiation-decorators.d.ts +94 -0
- package/lib/esm/types/adapter-express/express-utils/decorators.d.ts +257 -0
- package/lib/esm/types/adapter-express/express-utils/exception-filter-decorators.d.ts +6 -0
- package/lib/esm/types/adapter-express/express-utils/guard-context-factory.d.ts +17 -0
- package/lib/esm/types/adapter-express/express-utils/guard-middleware.d.ts +22 -0
- package/lib/esm/types/adapter-express/express-utils/guard-utils.d.ts +11 -0
- package/lib/esm/types/adapter-express/express-utils/http-context-store.d.ts +20 -0
- package/lib/esm/types/adapter-express/express-utils/http-status-middleware.d.ts +26 -0
- package/lib/esm/types/adapter-express/express-utils/httpResponseMessage.d.ts +14 -0
- package/lib/esm/types/adapter-express/express-utils/index.d.ts +30 -0
- package/lib/esm/types/adapter-express/express-utils/interceptor-middleware.d.ts +40 -0
- package/lib/esm/types/adapter-express/express-utils/interfaces.d.ts +115 -0
- package/lib/esm/types/adapter-express/express-utils/inversify-express-server.d.ts +172 -0
- package/lib/esm/types/adapter-express/express-utils/lazy-module-middleware.d.ts +122 -0
- package/lib/esm/types/adapter-express/express-utils/middleware-composition.d.ts +85 -0
- package/lib/esm/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
- package/lib/esm/types/adapter-express/express-utils/permission-preloader.middleware.d.ts +10 -0
- package/lib/esm/types/adapter-express/express-utils/resolver-multer.d.ts +7 -0
- package/lib/esm/types/adapter-express/express-utils/route-constraints.d.ts +98 -0
- package/lib/esm/types/adapter-express/express-utils/scope-extractor.d.ts +21 -0
- package/lib/esm/types/adapter-express/express-utils/scope-extractor.interface.d.ts +12 -0
- package/lib/esm/types/adapter-express/express-utils/setup-authorization.d.ts +34 -0
- package/lib/esm/types/adapter-express/express-utils/setup-event-system.d.ts +118 -0
- package/lib/esm/types/adapter-express/express-utils/setup-interceptors.d.ts +115 -0
- package/lib/esm/types/adapter-express/express-utils/setup-lazy-loading.d.ts +123 -0
- package/lib/esm/types/adapter-express/express-utils/utils.d.ts +24 -0
- package/lib/esm/types/adapter-express/express-utils/validation-decorators.d.ts +145 -0
- package/lib/esm/types/adapter-express/express-utils/validation-service.d.ts +88 -0
- package/lib/esm/types/adapter-express/index.d.ts +7 -0
- package/lib/esm/types/adapter-express/micro-api/application-express-micro-container.d.ts +47 -0
- package/lib/esm/types/adapter-express/micro-api/application-express-micro-route.d.ts +104 -0
- package/lib/esm/types/adapter-express/micro-api/application-express-micro.d.ts +72 -0
- package/lib/esm/types/adapter-express/micro-api/gateway/circuit-breaker.d.ts +111 -0
- package/lib/esm/types/adapter-express/micro-api/gateway/index.d.ts +5 -0
- package/lib/esm/types/adapter-express/micro-api/gateway/service-proxy.d.ts +83 -0
- package/lib/esm/types/adapter-express/micro-api/index.d.ts +7 -0
- package/lib/esm/types/adapter-express/micro-api/micro.d.ts +83 -0
- package/lib/esm/types/adapter-express/micro-api/queue/index.d.ts +5 -0
- package/lib/esm/types/adapter-express/micro-api/queue/queue.interface.d.ts +60 -0
- package/lib/esm/types/adapter-express/micro-api/queue/rabbitmq-consumer.d.ts +86 -0
- package/lib/esm/types/adapter-express/micro-api/serverless/aws-lambda.adapter.d.ts +77 -0
- package/lib/esm/types/adapter-express/micro-api/serverless/cloudflare.adapter.d.ts +64 -0
- package/lib/esm/types/adapter-express/micro-api/serverless/index.d.ts +6 -0
- package/lib/esm/types/adapter-express/micro-api/serverless/vercel.adapter.d.ts +56 -0
- package/lib/esm/types/adapter-express/micro-api/service-mesh/index.d.ts +5 -0
- package/lib/esm/types/adapter-express/micro-api/service-mesh/service-client.d.ts +122 -0
- package/lib/esm/types/adapter-express/micro-api/service-mesh/service-discovery.d.ts +150 -0
- package/lib/esm/types/adapter-express/middleware/index.d.ts +5 -0
- package/lib/esm/types/adapter-express/middleware/request-logging.middleware.d.ts +65 -0
- package/lib/esm/types/adapter-express/render/constants.d.ts +26 -0
- package/lib/esm/types/adapter-express/render/engine.d.ts +20 -0
- package/lib/esm/types/adapter-express/render/index.d.ts +5 -0
- package/lib/esm/types/adapter-express/render/resolve-render.d.ts +7 -0
- package/lib/esm/types/adapter-express/studio/index.d.ts +1 -0
- package/lib/esm/types/adapter-express/studio/studio-integration.d.ts +170 -0
- package/lib/esm/types/index.d.ts +1 -0
- package/lib/package.json +170 -146
- package/package.json +170 -146
- package/lib/cjs/di/di.interfaces.js +0 -10
- 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
|
|
31
|
-
const
|
|
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
|
|
35
|
-
const
|
|
36
|
-
const
|
|
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
|
|
61
|
-
* Use this method to configure global settings
|
|
62
|
-
*
|
|
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
|
|
239
|
+
* @returns {void}
|
|
66
240
|
* @public API
|
|
67
241
|
*/
|
|
68
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
93
|
-
*
|
|
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
|
-
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
282
|
+
serverShutdown(signal) { }
|
|
100
283
|
/**
|
|
101
|
-
*
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
523
|
+
process.exit(1);
|
|
189
524
|
}
|
|
190
|
-
|
|
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
|
|
196
|
-
const expressServer = new
|
|
197
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
*
|
|
309
|
-
* @
|
|
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
|
|
324
|
-
this.
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
}
|