@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
|
@@ -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 =
|
|
66
|
+
realPath = effectivePath;
|
|
54
67
|
}
|
|
55
|
-
else if (
|
|
68
|
+
else if (effectivePath === "/" || effectivePath === "") {
|
|
56
69
|
realPath = methodPath.startsWith("/") ? methodPath : `/${methodPath}`;
|
|
57
70
|
}
|
|
58
71
|
else {
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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}`, "
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
path:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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; } });
|