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