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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +61 -61
  3. package/lib/CHANGELOG.md +10 -5
  4. package/lib/README.md +61 -61
  5. package/lib/cjs/adapter-express/application-express.js +401 -45
  6. package/lib/cjs/adapter-express/express-utils/decorators.js +44 -15
  7. package/lib/cjs/adapter-express/express-utils/inversify-express-server.js +20 -4
  8. package/lib/cjs/adapter-express/express-utils/path-pattern-compat.js +129 -0
  9. package/lib/cjs/adapter-express/express-utils/route-constraints.js +12 -3
  10. package/lib/cjs/adapter-express/micro-api/application-express-micro.js +5 -9
  11. package/lib/cjs/adapter-express/micro-api/micro.js +96 -41
  12. package/lib/cjs/adapter-express/studio/index.js +2 -1
  13. package/lib/cjs/adapter-express/studio/studio-integration.js +64 -11
  14. package/lib/cjs/types/adapter-express/application-express.d.ts +51 -9
  15. package/lib/cjs/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
  16. package/lib/cjs/types/adapter-express/express-utils/route-constraints.d.ts +12 -3
  17. package/lib/cjs/types/adapter-express/micro-api/micro.d.ts +19 -2
  18. package/lib/cjs/types/adapter-express/studio/index.d.ts +1 -1
  19. package/lib/cjs/types/adapter-express/studio/studio-integration.d.ts +78 -0
  20. package/lib/esm/adapter-express/application-express.js +402 -46
  21. package/lib/esm/adapter-express/express-utils/decorators.js +44 -15
  22. package/lib/esm/adapter-express/express-utils/inversify-express-server.js +20 -4
  23. package/lib/esm/adapter-express/express-utils/path-pattern-compat.js +125 -0
  24. package/lib/esm/adapter-express/express-utils/route-constraints.js +12 -3
  25. package/lib/esm/adapter-express/micro-api/application-express-micro.js +6 -10
  26. package/lib/esm/adapter-express/micro-api/micro.js +97 -42
  27. package/lib/esm/adapter-express/studio/index.js +1 -1
  28. package/lib/esm/adapter-express/studio/studio-integration.js +63 -11
  29. package/lib/esm/types/adapter-express/application-express.d.ts +51 -9
  30. package/lib/esm/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
  31. package/lib/esm/types/adapter-express/express-utils/route-constraints.d.ts +12 -3
  32. package/lib/esm/types/adapter-express/micro-api/micro.d.ts +19 -2
  33. package/lib/esm/types/adapter-express/studio/index.d.ts +1 -1
  34. package/lib/esm/types/adapter-express/studio/studio-integration.d.ts +78 -0
  35. package/lib/package.json +24 -10
  36. package/package.json +25 -11
@@ -21,6 +21,7 @@ const core_1 = require("@expressots/core");
21
21
  const constants_js_1 = require("./constants.js");
22
22
  const resolver_multer_js_1 = require("./resolver-multer.js");
23
23
  const core_2 = require("@expressots/core");
24
+ const path_pattern_compat_js_1 = require("./path-pattern-compat.js");
24
25
  // Explicit type annotation: without this, the inferred type pulls a
25
26
  // non-portable path from @expressots/core's internal decorator_utils,
26
27
  // which TS2742 rejects under NodeNext when emitting .d.ts files.
@@ -35,9 +36,19 @@ function controller(path, ...middleware) {
35
36
  return (target) => {
36
37
  // Check for version metadata on the controller class
37
38
  const controllerVersion = Reflect.getOwnMetadata(constants_js_1.METADATA_KEY.version, target);
39
+ // Translate any inline regex constraints in the controller-level
40
+ // prefix (`@controller("/users/:tenant(\\d+)")`) into a
41
+ // request-time validator. Keeps `@controller("/")` and the common
42
+ // case allocation-free.
43
+ const split = (0, path_pattern_compat_js_1.splitPathConstraints)(path);
44
+ const constraintMiddleware = (0, path_pattern_compat_js_1.createPathConstraintMiddleware)(split.constraints);
45
+ const effectivePath = split.path;
46
+ const effectiveMiddleware = constraintMiddleware
47
+ ? [constraintMiddleware, ...middleware]
48
+ : middleware;
38
49
  const currentMetadata = {
39
- middleware,
40
- path,
50
+ middleware: effectiveMiddleware,
51
+ path: effectivePath,
41
52
  target: target,
42
53
  version: controllerVersion,
43
54
  };
@@ -47,17 +58,18 @@ function controller(path, ...middleware) {
47
58
  for (const key in pathMetadata) {
48
59
  if (statusCodeMetadata && statusCodeMetadata[key]) {
49
60
  const methodPath = pathMetadata[key]["path"];
50
- // Properly join controller and method paths
61
+ // Properly join controller and method paths. The controller
62
+ // path is the v8-cleaned `effectivePath` so the mapping key is
63
+ // consistent with what gets registered on Express.
51
64
  let realPath;
52
65
  if (methodPath === "/" || methodPath === "") {
53
- realPath = path;
66
+ realPath = effectivePath;
54
67
  }
55
- else if (path === "/" || path === "") {
68
+ else if (effectivePath === "/" || effectivePath === "") {
56
69
  realPath = methodPath.startsWith("/") ? methodPath : `/${methodPath}`;
57
70
  }
58
71
  else {
59
- // Normalize: remove trailing slash from controller, ensure method has leading slash
60
- const basePath = path.endsWith("/") ? path.slice(0, -1) : path;
72
+ const basePath = effectivePath.endsWith("/") ? effectivePath.slice(0, -1) : effectivePath;
61
73
  const subPath = methodPath.startsWith("/") ? methodPath : `/${methodPath}`;
62
74
  realPath = `${basePath}${subPath}`;
63
75
  }
@@ -222,11 +234,21 @@ function enhancedHttpMethod(method, path, ...middleware) {
222
234
  return (target, key) => {
223
235
  // Check for version metadata on the method
224
236
  const methodVersion = Reflect.getOwnMetadata(constants_js_1.METADATA_KEY.version, target, key);
237
+ // Express 5 / path-to-regexp v8 dropped inline regex constraints
238
+ // (`:id(\\d+)`). Split them out into a v8-compatible path + a
239
+ // request-time validator middleware so existing routes — and our
240
+ // {@link Patterns} / {@link pattern} public API — keep working.
241
+ const split = (0, path_pattern_compat_js_1.splitPathConstraints)(path);
242
+ const constraintMiddleware = (0, path_pattern_compat_js_1.createPathConstraintMiddleware)(split.constraints);
243
+ const effectivePath = split.path;
244
+ const effectiveMiddleware = constraintMiddleware
245
+ ? [constraintMiddleware, ...middleware]
246
+ : middleware;
225
247
  const metadata = {
226
248
  key: String(key),
227
249
  method,
228
- middleware,
229
- path,
250
+ middleware: effectiveMiddleware,
251
+ path: effectivePath,
230
252
  target: target,
231
253
  version: methodVersion,
232
254
  };
@@ -234,14 +256,14 @@ function enhancedHttpMethod(method, path, ...middleware) {
234
256
  let pathMetadata = Reflect.getOwnMetadata(constants_js_1.HTTP_CODE_METADATA.path, Reflect);
235
257
  if (pathMetadata) {
236
258
  pathMetadata[key] = {
237
- path,
259
+ path: effectivePath,
238
260
  method,
239
261
  };
240
262
  }
241
263
  else {
242
264
  pathMetadata = {};
243
265
  pathMetadata[key] = {
244
- path,
266
+ path: effectivePath,
245
267
  method,
246
268
  };
247
269
  }
@@ -277,11 +299,18 @@ function Method(method, path, ...middleware) {
277
299
  return (target, key) => {
278
300
  // Check for version metadata on the method
279
301
  const methodVersion = Reflect.getOwnMetadata(constants_js_1.METADATA_KEY.version, target, key);
302
+ // Same path-to-regexp v8 compatibility shim as `enhancedHttpMethod`.
303
+ const split = (0, path_pattern_compat_js_1.splitPathConstraints)(path);
304
+ const constraintMiddleware = (0, path_pattern_compat_js_1.createPathConstraintMiddleware)(split.constraints);
305
+ const effectivePath = split.path;
306
+ const effectiveMiddleware = constraintMiddleware
307
+ ? [constraintMiddleware, ...middleware]
308
+ : middleware;
280
309
  const metadata = {
281
310
  key: String(key),
282
311
  method,
283
- middleware,
284
- path,
312
+ middleware: effectiveMiddleware,
313
+ path: effectivePath,
285
314
  target: target,
286
315
  version: methodVersion,
287
316
  };
@@ -289,14 +318,14 @@ function Method(method, path, ...middleware) {
289
318
  let pathMetadata = Reflect.getOwnMetadata(constants_js_1.HTTP_CODE_METADATA.path, Reflect);
290
319
  if (pathMetadata) {
291
320
  pathMetadata[key] = {
292
- path,
321
+ path: effectivePath,
293
322
  method,
294
323
  };
295
324
  }
296
325
  else {
297
326
  pathMetadata = {};
298
327
  pathMetadata[key] = {
299
- path,
328
+ path: effectivePath,
300
329
  method,
301
330
  };
302
331
  }
@@ -146,7 +146,12 @@ class InversifyExpressServer {
146
146
  // request and attach it via a WeakMap (see ./http-context-store).
147
147
  // This is cheaper than the previous `Reflect.defineMetadata` call
148
148
  // because it bypasses reflect-metadata's string-keyed map.
149
- this._app.all("*", (req, res, next) => {
149
+ //
150
+ // We use `app.use(handler)` (no path arg) which runs for every request
151
+ // regardless of method or path — the same behavior as the previous
152
+ // `app.all("*", ...)` registration but without a path-to-regexp pattern,
153
+ // so it is forward-compatible with Express 5 / path-to-regexp v8.
154
+ this._app.use((req, res, next) => {
150
155
  this._createHttpContext(req, res, next)
151
156
  .then((httpContext) => {
152
157
  (0, http_context_store_js_1.setHttpContext)(req, httpContext);
@@ -785,13 +790,20 @@ class InversifyExpressServer {
785
790
  }
786
791
  // Execute guard middleware if guards exist
787
792
  if (guardMiddleware && allGuards.length > 0) {
788
- return guardMiddleware(req, res, async (err) => {
793
+ // Express 5 typings expect the handler to return `void`. We cannot
794
+ // `return` the call because the express handler signature is
795
+ // `(req, res, next) => void | Promise<void>` in v5; chaining the
796
+ // expression off `return` makes TS infer `unknown`. Invoke it
797
+ // for-effect and return.
798
+ guardMiddleware(req, res, async (err) => {
789
799
  if (err) {
790
- return next(err);
800
+ next(err);
801
+ return;
791
802
  }
792
803
  // Guards passed, continue to route handler
793
804
  await this.executeRouteHandler(req, res, next, controllerName, key, parameterMetadata, controllerConstructor);
794
805
  });
806
+ return;
795
807
  }
796
808
  // No guards, execute route handler directly
797
809
  await this.executeRouteHandler(req, res, next, controllerName, key, parameterMetadata, controllerConstructor);
@@ -892,7 +904,11 @@ class InversifyExpressServer {
892
904
  // Extract resource info from path for helpful error message
893
905
  const pathParts = req.path.split("/").filter(Boolean);
894
906
  const resource = pathParts[pathParts.length - 2] || "Resource";
895
- const id = req.params?.id || pathParts[pathParts.length - 1];
907
+ // Express 5's path-to-regexp v8 widens param values to
908
+ // `string | string[]` because array-style params are now first
909
+ // class. Coerce to string for the NotFoundError signature.
910
+ const rawId = req.params?.id ?? pathParts[pathParts.length - 1];
911
+ const id = Array.isArray(rawId) ? rawId.join("/") : rawId;
896
912
  throw new core_1.NotFoundError(resource, id);
897
913
  }
898
914
  // For other methods (DELETE, PUT, PATCH), undefined is valid (204 No Content)
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.splitPathConstraints = splitPathConstraints;
4
+ exports.createPathConstraintMiddleware = createPathConstraintMiddleware;
5
+ /**
6
+ * Split `:name(regex)` segments out of an Express-style route path.
7
+ *
8
+ * The walker honours balanced parens inside the regex (e.g.
9
+ * `(\\d{4})` or `((a|b)+)`), which is more forgiving than a naive
10
+ * single-pass regex match would be. Returns the original path and an
11
+ * empty constraints list when no inline patterns are found, so this is
12
+ * a no-op for the common case.
13
+ */
14
+ function splitPathConstraints(path) {
15
+ // Defensive: hand a non-string straight back. The decorators occasionally
16
+ // see `undefined` or `null` paths in older test fixtures and we don't
17
+ // want to crash decorator-time evaluation just because of input shape.
18
+ if (typeof path !== "string") {
19
+ return { path: path, constraints: [] };
20
+ }
21
+ const constraints = [];
22
+ let out = "";
23
+ let i = 0;
24
+ while (i < path.length) {
25
+ const ch = path[i];
26
+ if (ch !== ":") {
27
+ out += ch;
28
+ i++;
29
+ continue;
30
+ }
31
+ // Found a `:` — scan the following identifier (matches `[A-Za-z0-9_]+`,
32
+ // same character class path-to-regexp v8 accepts).
33
+ const start = i;
34
+ i++;
35
+ let nameEnd = i;
36
+ while (nameEnd < path.length && /[A-Za-z0-9_]/.test(path[nameEnd])) {
37
+ nameEnd++;
38
+ }
39
+ if (nameEnd === i) {
40
+ // `:` not followed by an identifier — leave it to path-to-regexp.
41
+ out += ":";
42
+ continue;
43
+ }
44
+ const paramName = path.slice(i, nameEnd);
45
+ out += `:${paramName}`;
46
+ i = nameEnd;
47
+ // Optional inline `(...regex...)` immediately after the name.
48
+ if (path[i] !== "(") {
49
+ continue;
50
+ }
51
+ let depth = 1;
52
+ let j = i + 1;
53
+ while (j < path.length && depth > 0) {
54
+ const c = path[j];
55
+ if (c === "\\" && j + 1 < path.length) {
56
+ j += 2;
57
+ continue;
58
+ }
59
+ if (c === "(")
60
+ depth++;
61
+ else if (c === ")")
62
+ depth--;
63
+ j++;
64
+ }
65
+ if (depth !== 0) {
66
+ // Unbalanced — leave the original path intact and let path-to-regexp
67
+ // raise its own (better-located) error.
68
+ out += path.slice(i);
69
+ i = path.length;
70
+ break;
71
+ }
72
+ const rawPattern = path.slice(i + 1, j - 1);
73
+ try {
74
+ constraints.push({
75
+ paramName,
76
+ regex: new RegExp(`^(?:${rawPattern})$`),
77
+ rawPattern,
78
+ });
79
+ }
80
+ catch {
81
+ // The regex inside the parens is malformed. We don't want a
82
+ // decorator-time crash — fall back to dropping the constraint so
83
+ // the route at least registers; runtime validation simply won't
84
+ // run for this param.
85
+ }
86
+ // Consume the `(...)` segment without re-emitting it into `out`,
87
+ // since path-to-regexp v8 doesn't accept the syntax.
88
+ i = j;
89
+ // Quietly consume a trailing redundant `?` (older code wrote
90
+ // `:foo(\\d+)?`); v8 spells optional segments with `{...}` braces,
91
+ // and "drop the constraint, keep the param" is the sane fallback.
92
+ if (path[i] === "?") {
93
+ i++;
94
+ }
95
+ // Path-to-regexp v6 supported quantifier suffixes (`+`, `*`); v8
96
+ // dropped them. We strip them silently for the same reason.
97
+ while (path[i] === "+" || path[i] === "*") {
98
+ i++;
99
+ }
100
+ void start;
101
+ }
102
+ return { path: out, constraints };
103
+ }
104
+ /**
105
+ * Build a middleware that enforces the given param-level regex
106
+ * constraints on `req.params`. Returns `null` when the list is empty
107
+ * (so callers can avoid wiring an unnecessary middleware).
108
+ *
109
+ * When a constraint fails, the middleware delegates to `next()` without
110
+ * a value; the framework's NotFound handler then converts that into a
111
+ * 404 — same observable behaviour as Express 4's "no route matched".
112
+ */
113
+ function createPathConstraintMiddleware(constraints) {
114
+ if (constraints.length === 0)
115
+ return null;
116
+ return (req, res, next) => {
117
+ for (const c of constraints) {
118
+ const value = req.params?.[c.paramName];
119
+ // Express 5 sometimes hands back `string[]` for splat params, but
120
+ // inline-regex params are always scalar strings; coerce defensively.
121
+ const scalar = Array.isArray(value) ? value.join("/") : value;
122
+ if (typeof scalar !== "string" || !c.regex.test(scalar)) {
123
+ // Skip to the next handler so the framework's 404 path runs.
124
+ return next("route");
125
+ }
126
+ }
127
+ next();
128
+ };
129
+ }
@@ -4,7 +4,16 @@ exports.Patterns = void 0;
4
4
  exports.pattern = pattern;
5
5
  /**
6
6
  * Route parameter patterns for common use cases.
7
- * These are Express regex patterns that can be used in route paths.
7
+ *
8
+ * Express 5 / path-to-regexp v8 dropped the inline-regex form
9
+ * (`:id(\\d+)`), so the framework no longer hands these patterns to
10
+ * the underlying matcher verbatim. Instead, the HTTP-method decorators
11
+ * (`@Get`, `@Post`, …) parse the constraint out of the path at decorator
12
+ * time, register the route under a plain `:id` placeholder, and inject
13
+ * a small validator middleware that 404s when the captured value
14
+ * doesn't match. The user-facing semantics are unchanged: a path that
15
+ * uses `Patterns.NUMERIC_ID` still rejects `/users/abc` and only
16
+ * dispatches the handler for matches like `/users/123`.
8
17
  *
9
18
  * @example
10
19
  * ```typescript
@@ -12,12 +21,12 @@ exports.pattern = pattern;
12
21
  *
13
22
  * @Get(`/users/${pattern("id", Patterns.NUMERIC_ID)}`)
14
23
  * getUserById(@param("id") id: number) {
15
- * // Only matches numeric IDs like /users/123
24
+ * // Only dispatches for numeric IDs like /users/123
16
25
  * }
17
26
  *
18
27
  * @Get(`/documents/${pattern("uuid", Patterns.UUID)}`)
19
28
  * getDocument(@param("uuid") uuid: string) {
20
- * // Only matches valid UUIDs
29
+ * // Only dispatches for valid UUIDs
21
30
  * }
22
31
  * ```
23
32
  *
@@ -18,7 +18,6 @@ class AppExpressMicro {
18
18
  * @private
19
19
  */
20
20
  handleExit() {
21
- this.logger.info("Server shutting down.", "MicroAPI");
22
21
  process.exit(0);
23
22
  }
24
23
  /**
@@ -116,7 +115,6 @@ class AppExpressMicro {
116
115
  */
117
116
  async listen(port, appInfo) {
118
117
  const logger = new core_1.Logger();
119
- const console = new core_1.Console();
120
118
  const normalizedPort = typeof port === "string" ? parseInt(port, 10) : port;
121
119
  this.configureMiddleware();
122
120
  this.routeManager.applyRoutes();
@@ -124,7 +122,7 @@ class AppExpressMicro {
124
122
  this.app.use(this.Middleware.getErrorHandler());
125
123
  }
126
124
  return new Promise((resolve, reject) => {
127
- this.httpServer = this.app.listen(normalizedPort, async () => {
125
+ this.httpServer = this.app.listen(normalizedPort, () => {
128
126
  const address = this.httpServer.address();
129
127
  if (typeof address === "object" && address?.port) {
130
128
  this.port = address.port;
@@ -132,18 +130,16 @@ class AppExpressMicro {
132
130
  else {
133
131
  this.port = normalizedPort;
134
132
  }
135
- // Display startup message using Console class
136
- await console.messageServer(this.port, this.environment, {
137
- appName: appInfo?.appName || "ExpressoTS Micro",
138
- appVersion: appInfo?.appVersion || "1.0.0",
139
- });
133
+ const name = appInfo?.appName || "ExpressoTS Micro";
134
+ const version = appInfo?.appVersion || "1.0.0";
135
+ logger.info(`${name} version ${version} is running on port ${this.port} - Environment: ${this.environment}`, "micro");
140
136
  ["SIGTERM", "SIGHUP", "SIGBREAK", "SIGQUIT", "SIGINT"].forEach((signal) => {
141
137
  process.on(signal, this.handleExit.bind(this));
142
138
  });
143
139
  resolve();
144
140
  });
145
141
  this.httpServer.on("error", (error) => {
146
- logger.error(`Error starting server: ${error.message}`, "MicroAPI");
142
+ logger.error(`Error starting server: ${error.message}`, "micro");
147
143
  reject(error);
148
144
  });
149
145
  });
@@ -7,6 +7,7 @@ exports.micro = micro;
7
7
  const core_1 = require("@expressots/core");
8
8
  const express_1 = __importDefault(require("express"));
9
9
  const application_express_js_1 = require("../application-express.js");
10
+ const index_js_1 = require("../studio/index.js");
10
11
  /**
11
12
  * Create a new micro API instance
12
13
  *
@@ -27,10 +28,21 @@ function micro(config) {
27
28
  application_express_js_1.AppExpress.disableBuffering();
28
29
  const app = (0, express_1.default)();
29
30
  const logger = new core_1.Logger();
30
- const console = new core_1.Console();
31
31
  const globalPrefix = config?.globalPrefix?.replace(/\/$/, "") || "";
32
32
  let httpServer;
33
33
  let errorHandler = null;
34
+ let studioConfig = config?.studio ?? {};
35
+ // Lazy proxy for the Studio Agent middleware. Installed at position 0 so
36
+ // it always runs before route handlers — even though initializeStudio()
37
+ // is only called in listen(). Once the agent starts, it sets the real
38
+ // handler; until then the proxy is a no-op pass-through.
39
+ let studioMiddlewareDelegate = null;
40
+ app.use((req, res, next) => {
41
+ if (studioMiddlewareDelegate) {
42
+ return studioMiddlewareDelegate(req, res, next);
43
+ }
44
+ next();
45
+ });
34
46
  // Auto-enable JSON parsing by default
35
47
  if (config?.autoParseJson !== false) {
36
48
  app.use(express_1.default.json());
@@ -93,30 +105,8 @@ function micro(config) {
93
105
  if (res.headersSent) {
94
106
  return next();
95
107
  }
96
- const suggestionsConfig = (0, core_1.getDefaultSuggestionsConfig)();
97
- if (!suggestionsConfig.enabled) {
98
- return next();
99
- }
100
108
  const requestedPath = req.originalUrl || req.url;
101
109
  const requestedMethod = req.method;
102
- const hints = (0, core_1.getErrorHints)(new Error(`Route '${requestedMethod} ${requestedPath}' not found`), {
103
- path: requestedPath,
104
- method: requestedMethod,
105
- statusCode: 404,
106
- }, suggestionsConfig);
107
- if (hints.length > 0) {
108
- try {
109
- const formatted = (0, core_1.formatSuggestions)(hints);
110
- if (formatted) {
111
- logger.warn(`Route not found: ${requestedMethod} ${requestedPath}${formatted}`, "router-404");
112
- }
113
- }
114
- catch {
115
- // best-effort logging
116
- }
117
- }
118
- const routeSuggestion = hints.find((hint) => hint.type === "route");
119
- const actionHint = hints.find((hint) => hint.type === "hint");
120
110
  const body = {
121
111
  type: "https://expressots.dev/errors/not-found",
122
112
  title: "Route Not Found",
@@ -125,26 +115,58 @@ function micro(config) {
125
115
  instance: requestedPath,
126
116
  timestamp: new Date().toISOString(),
127
117
  };
128
- if (routeSuggestion?.routes && routeSuggestion.routes.length > 0) {
129
- body.suggestions = routeSuggestion.routes.map((suggestion) => ({
130
- method: suggestion.route.method,
131
- path: suggestion.route.fullPath || suggestion.route.path,
132
- similarity: Math.round(suggestion.similarity * 100),
133
- reason: suggestion.reason,
134
- }));
135
- }
136
- else if (actionHint?.actions && actionHint.actions.length > 0) {
137
- body.actions = actionHint.actions;
118
+ const suggestionsConfig = (0, core_1.getDefaultSuggestionsConfig)();
119
+ if (suggestionsConfig.enabled) {
120
+ const hints = (0, core_1.getErrorHints)(new Error(`Route '${requestedMethod} ${requestedPath}' not found`), {
121
+ path: requestedPath,
122
+ method: requestedMethod,
123
+ statusCode: 404,
124
+ }, suggestionsConfig);
125
+ if (hints.length > 0) {
126
+ try {
127
+ const formatted = (0, core_1.formatSuggestions)(hints);
128
+ if (formatted) {
129
+ logger.warn(`Route not found: ${requestedMethod} ${requestedPath}${formatted}`, "router-404");
130
+ }
131
+ }
132
+ catch {
133
+ // best-effort logging
134
+ }
135
+ const routeSuggestion = hints.find((hint) => hint.type === "route");
136
+ const actionHint = hints.find((hint) => hint.type === "hint");
137
+ if (routeSuggestion?.routes && routeSuggestion.routes.length > 0) {
138
+ body.suggestions = routeSuggestion.routes.map((suggestion) => ({
139
+ method: suggestion.route.method,
140
+ path: suggestion.route.fullPath || suggestion.route.path,
141
+ similarity: Math.round(suggestion.similarity * 100),
142
+ reason: suggestion.reason,
143
+ }));
144
+ }
145
+ else if (actionHint?.actions && actionHint.actions.length > 0) {
146
+ body.actions = actionHint.actions;
147
+ }
148
+ }
138
149
  }
139
150
  res.status(404).type("application/json").send(JSON.stringify(body));
140
151
  });
141
152
  };
142
153
  /**
143
- * Handle server shutdown gracefully
154
+ * Handle server shutdown. In development, exit immediately for fast
155
+ * hot-reload. In production, drain connections before exiting.
144
156
  */
145
157
  const handleExit = () => {
146
- logger.info("Server shutting down", "micro");
147
- process.exit(0);
158
+ const environment = config?.environment || process.env.NODE_ENV || "development";
159
+ void (0, index_js_1.stopStudio)();
160
+ if (environment === "development") {
161
+ process.exit(0);
162
+ }
163
+ if (httpServer) {
164
+ httpServer.close(() => process.exit(0));
165
+ setTimeout(() => process.exit(0), 5000).unref();
166
+ }
167
+ else {
168
+ process.exit(0);
169
+ }
148
170
  };
149
171
  const microApp = {
150
172
  get: (path, ...handlers) => route("get", path, ...handlers),
@@ -167,6 +189,24 @@ function micro(config) {
167
189
  },
168
190
  async listen(port, appInfo) {
169
191
  const normalizedPort = typeof port === "string" ? parseInt(port, 10) : port;
192
+ const listenStartedAt = Date.now();
193
+ // Initialize Studio Agent. The agent's middleware is registered via
194
+ // app.use() inside initializeStudio, but that lands AFTER the user's
195
+ // routes in the Express stack. Our lazy proxy (installed at position 0
196
+ // during micro() creation) ensures CORS headers are injected before
197
+ // any route handler sends a response.
198
+ const studioStarted = await (0, index_js_1.initializeStudio)(app, {
199
+ ...studioConfig,
200
+ serviceName: studioConfig.serviceName ?? "expressots-micro",
201
+ appPort: normalizedPort,
202
+ globalPrefix: globalPrefix || undefined,
203
+ });
204
+ if (studioStarted) {
205
+ const agent = (0, index_js_1.getStudioAgent)();
206
+ if (agent) {
207
+ studioMiddlewareDelegate = agent.createMiddleware();
208
+ }
209
+ }
170
210
  // Install the 404 fallback before the user error handler so unmatched
171
211
  // routes get suggestions instead of falling through to the default
172
212
  // Express HTML or - worse - to a regular middleware that arity-confused
@@ -189,11 +229,19 @@ function micro(config) {
189
229
  const address = httpServer.address();
190
230
  const actualPort = typeof address === "object" && address?.port ? address.port : normalizedPort;
191
231
  if (config?.showBanner !== false) {
192
- await console.messageServer(actualPort, "development", {
193
- appName: appInfo?.appName || "ExpressoTS Micro",
194
- appVersion: appInfo?.appVersion || "1.0.0",
195
- });
232
+ const name = appInfo?.appName || "ExpressoTS Micro";
233
+ const version = appInfo?.appVersion || "1.0.0";
234
+ const environment = config?.environment || process.env.NODE_ENV || "development";
235
+ logger.info(`${name} version ${version} is running on port ${actualPort} - Environment: ${environment}`, "micro");
196
236
  }
237
+ // Push runtime info to Studio Agent now that we know the actual port
238
+ (0, index_js_1.reportStudioRuntimeInfo)({
239
+ appPort: actualPort,
240
+ globalPrefix: globalPrefix || undefined,
241
+ startupMs: Date.now() - listenStartedAt,
242
+ });
243
+ // Re-scan routes so Studio sees the fully-populated Express router
244
+ void (0, index_js_1.rescanStudioRoutes)();
197
245
  // Handle graceful shutdown
198
246
  ["SIGTERM", "SIGHUP", "SIGBREAK", "SIGQUIT", "SIGINT"].forEach((signal) => {
199
247
  process.on(signal, handleExit);
@@ -207,11 +255,18 @@ function micro(config) {
207
255
  });
208
256
  },
209
257
  getHttpServer() {
210
- return httpServer;
258
+ return httpServer ?? null;
211
259
  },
212
260
  getApp() {
213
261
  return app;
214
262
  },
263
+ setStudio(cfg) {
264
+ studioConfig = cfg;
265
+ return microApp;
266
+ },
267
+ isStudioEnabled() {
268
+ return (0, index_js_1.isStudioEnabled)();
269
+ },
215
270
  };
216
271
  return microApp;
217
272
  }
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.reportStudioRuntimeInfo = exports.getStudioAgent = exports.isStudioEnabled = exports.stopStudio = exports.initializeStudio = void 0;
3
+ exports.rescanStudioRoutes = exports.reportStudioRuntimeInfo = exports.getStudioAgent = exports.isStudioEnabled = exports.stopStudio = exports.initializeStudio = void 0;
4
4
  var studio_integration_js_1 = require("./studio-integration.js");
5
5
  Object.defineProperty(exports, "initializeStudio", { enumerable: true, get: function () { return studio_integration_js_1.initializeStudio; } });
6
6
  Object.defineProperty(exports, "stopStudio", { enumerable: true, get: function () { return studio_integration_js_1.stopStudio; } });
7
7
  Object.defineProperty(exports, "isStudioEnabled", { enumerable: true, get: function () { return studio_integration_js_1.isStudioEnabled; } });
8
8
  Object.defineProperty(exports, "getStudioAgent", { enumerable: true, get: function () { return studio_integration_js_1.getStudioAgent; } });
9
9
  Object.defineProperty(exports, "reportStudioRuntimeInfo", { enumerable: true, get: function () { return studio_integration_js_1.reportStudioRuntimeInfo; } });
10
+ Object.defineProperty(exports, "rescanStudioRoutes", { enumerable: true, get: function () { return studio_integration_js_1.rescanStudioRoutes; } });