@expressots/core 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 (134) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +66 -66
  3. package/lib/CHANGELOG.md +774 -774
  4. package/lib/README.md +66 -66
  5. package/lib/cjs/application/application-factory.js +6 -0
  6. package/lib/cjs/application/bootstrap.js +117 -213
  7. package/lib/cjs/config/define-config.js +1 -1
  8. package/lib/cjs/config/env-field-builders.js +47 -0
  9. package/lib/cjs/config/index.js +7 -1
  10. package/lib/cjs/framework-version.js +10 -0
  11. package/lib/cjs/lazy-loading/index.js +5 -1
  12. package/lib/cjs/lazy-loading/lazy-module-helpers.js +49 -0
  13. package/lib/cjs/middleware/index.js +8 -9
  14. package/lib/cjs/middleware/middleware-service.js +68 -12
  15. package/lib/cjs/middleware/presets-standalone.js +93 -0
  16. package/lib/cjs/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
  17. package/lib/cjs/provider/db-in-memory/index.js +11 -1
  18. package/lib/cjs/provider/db-in-memory/query/query-engine.js +28 -0
  19. package/lib/cjs/provider/db-in-memory/schema/decorators.js +18 -0
  20. package/lib/cjs/provider/db-in-memory/storage/index.js +3 -1
  21. package/lib/cjs/provider/db-in-memory/storage/memory-store.js +72 -1
  22. package/lib/cjs/provider/logger/logger.banner.js +40 -31
  23. package/lib/cjs/provider/logger/logger.config.js +11 -1
  24. package/lib/cjs/provider/logger/logger.formatter.js +22 -1
  25. package/lib/cjs/provider/logger/logger.provider.js +59 -9
  26. package/lib/cjs/provider/logger/transports/console.transport.js +69 -6
  27. package/lib/cjs/provider/logger/transports/file.transport.js +27 -18
  28. package/lib/cjs/provider/logger/utils/log-levels.js +6 -5
  29. package/lib/cjs/provider/validation/adapters/index.js +12 -5
  30. package/lib/cjs/provider/validation/adapters/yup.adapter.js +118 -0
  31. package/lib/cjs/provider/validation/adapters/zod.adapter.js +137 -0
  32. package/lib/cjs/provider/validation/index.js +5 -1
  33. package/lib/cjs/render/adapters/react-adapter.js +14 -14
  34. package/lib/cjs/render/features/type-generator.js +30 -30
  35. package/lib/cjs/render/features/view-debugger.js +75 -55
  36. package/lib/cjs/testing/fluent-request.js +7 -0
  37. package/lib/cjs/testing/snapshot-request.js +2 -0
  38. package/lib/cjs/types/application/application-factory.d.ts +6 -0
  39. package/lib/cjs/types/application/bootstrap.d.ts +196 -24
  40. package/lib/cjs/types/config/config.interfaces.d.ts +7 -1
  41. package/lib/cjs/types/config/env-field-builders.d.ts +39 -0
  42. package/lib/cjs/types/config/index.d.ts +1 -1
  43. package/lib/cjs/types/framework-version.d.ts +7 -0
  44. package/lib/cjs/types/lazy-loading/index.d.ts +1 -0
  45. package/lib/cjs/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
  46. package/lib/cjs/types/middleware/index.d.ts +1 -1
  47. package/lib/cjs/types/middleware/middleware-service.d.ts +21 -0
  48. package/lib/cjs/types/middleware/presets-standalone.d.ts +75 -0
  49. package/lib/cjs/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
  50. package/lib/cjs/types/provider/db-in-memory/index.d.ts +9 -1
  51. package/lib/cjs/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
  52. package/lib/cjs/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
  53. package/lib/cjs/types/provider/db-in-memory/storage/index.d.ts +1 -1
  54. package/lib/cjs/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
  55. package/lib/cjs/types/provider/logger/logger.banner.d.ts +1 -1
  56. package/lib/cjs/types/provider/logger/logger.config.d.ts +7 -0
  57. package/lib/cjs/types/provider/logger/logger.formatter.d.ts +10 -0
  58. package/lib/cjs/types/provider/logger/logger.provider.d.ts +32 -1
  59. package/lib/cjs/types/provider/logger/transports/console.transport.d.ts +7 -0
  60. package/lib/cjs/types/provider/logger/utils/log-levels.d.ts +3 -3
  61. package/lib/cjs/types/provider/validation/adapters/index.d.ts +7 -4
  62. package/lib/cjs/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
  63. package/lib/cjs/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
  64. package/lib/cjs/types/provider/validation/index.d.ts +1 -1
  65. package/lib/cjs/types/render/features/view-debugger.d.ts +10 -0
  66. package/lib/cjs/types/testing/testing.interfaces.d.ts +31 -6
  67. package/lib/esm/application/application-factory.js +6 -0
  68. package/lib/esm/application/bootstrap.js +117 -213
  69. package/lib/esm/config/define-config.js +1 -1
  70. package/lib/esm/config/env-field-builders.js +48 -0
  71. package/lib/esm/config/index.js +6 -1
  72. package/lib/esm/framework-version.js +7 -0
  73. package/lib/esm/lazy-loading/index.js +2 -0
  74. package/lib/esm/lazy-loading/lazy-module-helpers.js +45 -0
  75. package/lib/esm/middleware/index.js +3 -2
  76. package/lib/esm/middleware/middleware-service.js +68 -12
  77. package/lib/esm/middleware/presets-standalone.js +87 -0
  78. package/lib/esm/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
  79. package/lib/esm/provider/db-in-memory/index.js +9 -1
  80. package/lib/esm/provider/db-in-memory/query/query-engine.js +28 -0
  81. package/lib/esm/provider/db-in-memory/schema/decorators.js +18 -0
  82. package/lib/esm/provider/db-in-memory/storage/index.js +1 -1
  83. package/lib/esm/provider/db-in-memory/storage/memory-store.js +75 -0
  84. package/lib/esm/provider/logger/logger.banner.js +40 -31
  85. package/lib/esm/provider/logger/logger.config.js +12 -2
  86. package/lib/esm/provider/logger/logger.formatter.js +22 -1
  87. package/lib/esm/provider/logger/logger.provider.js +61 -10
  88. package/lib/esm/provider/logger/transports/console.transport.js +69 -6
  89. package/lib/esm/provider/logger/transports/file.transport.js +27 -18
  90. package/lib/esm/provider/logger/utils/log-levels.js +6 -5
  91. package/lib/esm/provider/validation/adapters/index.js +7 -4
  92. package/lib/esm/provider/validation/adapters/yup.adapter.js +111 -0
  93. package/lib/esm/provider/validation/adapters/zod.adapter.js +130 -0
  94. package/lib/esm/provider/validation/index.js +1 -1
  95. package/lib/esm/render/adapters/react-adapter.js +14 -14
  96. package/lib/esm/render/features/type-generator.js +30 -30
  97. package/lib/esm/render/features/view-debugger.js +75 -55
  98. package/lib/esm/testing/fluent-request.js +7 -0
  99. package/lib/esm/testing/snapshot-request.js +2 -0
  100. package/lib/esm/types/application/application-factory.d.ts +6 -0
  101. package/lib/esm/types/application/bootstrap.d.ts +196 -24
  102. package/lib/esm/types/config/config.interfaces.d.ts +7 -1
  103. package/lib/esm/types/config/env-field-builders.d.ts +39 -0
  104. package/lib/esm/types/config/index.d.ts +1 -1
  105. package/lib/esm/types/framework-version.d.ts +7 -0
  106. package/lib/esm/types/lazy-loading/index.d.ts +1 -0
  107. package/lib/esm/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
  108. package/lib/esm/types/middleware/index.d.ts +1 -1
  109. package/lib/esm/types/middleware/middleware-service.d.ts +21 -0
  110. package/lib/esm/types/middleware/presets-standalone.d.ts +75 -0
  111. package/lib/esm/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
  112. package/lib/esm/types/provider/db-in-memory/index.d.ts +9 -1
  113. package/lib/esm/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
  114. package/lib/esm/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
  115. package/lib/esm/types/provider/db-in-memory/storage/index.d.ts +1 -1
  116. package/lib/esm/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
  117. package/lib/esm/types/provider/logger/logger.banner.d.ts +1 -1
  118. package/lib/esm/types/provider/logger/logger.config.d.ts +7 -0
  119. package/lib/esm/types/provider/logger/logger.formatter.d.ts +10 -0
  120. package/lib/esm/types/provider/logger/logger.provider.d.ts +32 -1
  121. package/lib/esm/types/provider/logger/transports/console.transport.d.ts +7 -0
  122. package/lib/esm/types/provider/logger/utils/log-levels.d.ts +3 -3
  123. package/lib/esm/types/provider/validation/adapters/index.d.ts +7 -4
  124. package/lib/esm/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
  125. package/lib/esm/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
  126. package/lib/esm/types/provider/validation/index.d.ts +1 -1
  127. package/lib/esm/types/render/features/view-debugger.d.ts +10 -0
  128. package/lib/esm/types/testing/testing.interfaces.d.ts +31 -6
  129. package/lib/package.json +23 -8
  130. package/package.json +23 -8
  131. package/lib/cjs/middleware/middleware-presets.js +0 -294
  132. package/lib/cjs/types/middleware/middleware-presets.d.ts +0 -90
  133. package/lib/esm/middleware/middleware-presets.js +0 -286
  134. package/lib/esm/types/middleware/middleware-presets.d.ts +0 -90
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * Yup Validation Adapter
4
+ * @module @expressots/core/validation
5
+ *
6
+ * Built-in adapter for the [yup](https://github.com/jquense/yup) validation
7
+ * library. `yup` is declared as an *optional* peer dependency: install it
8
+ * explicitly (`npm i yup`) to enable this adapter.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import * as yup from "yup";
13
+ * import { validationRegistry, createYupValidator } from "@expressots/core";
14
+ *
15
+ * validationRegistry.register(createYupValidator());
16
+ *
17
+ * const CreateUserSchema = yup.object({
18
+ * email: yup.string().email().required(),
19
+ * age: yup.number().integer().min(18).required(),
20
+ * });
21
+ *
22
+ * @controller("/users")
23
+ * class UsersController {
24
+ * @Post("/")
25
+ * create(@body(CreateUserSchema) input: yup.InferType<typeof CreateUserSchema>) {
26
+ * // input is fully typed and validated
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.YupValidatorAdapter = void 0;
33
+ exports.createYupValidator = createYupValidator;
34
+ /**
35
+ * Adapter for [yup](https://github.com/jquense/yup). Detects yup schemas at
36
+ * runtime by checking for the `__isYupSchema__` brand or the presence of a
37
+ * `validate` + `cast` method pair.
38
+ *
39
+ * @public API
40
+ */
41
+ class YupValidatorAdapter {
42
+ constructor() {
43
+ this.name = "yup";
44
+ this.priority = 80;
45
+ }
46
+ canHandle(schema) {
47
+ if (schema === null || typeof schema !== "object")
48
+ return false;
49
+ const candidate = schema;
50
+ if (candidate.__isYupSchema__ === true)
51
+ return true;
52
+ return (typeof candidate.validate === "function" &&
53
+ typeof candidate.cast === "function");
54
+ }
55
+ async validate(data, schema, options) {
56
+ if (typeof schema.validate !== "function") {
57
+ return {
58
+ success: false,
59
+ errors: [
60
+ {
61
+ path: "",
62
+ message: "YupValidatorAdapter received a schema with no validate() method.",
63
+ code: "invalid_schema",
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ try {
69
+ const yupOptions = {
70
+ abortEarly: options?.abortEarly ?? false,
71
+ stripUnknown: options?.stripUnknown ?? false,
72
+ };
73
+ const validated = await schema.validate(data, yupOptions);
74
+ return { success: true, data: validated };
75
+ }
76
+ catch (error) {
77
+ const yupError = error;
78
+ // yup batches per-field issues into `inner` when abortEarly is false,
79
+ // and emits a single message otherwise.
80
+ const issues = yupError?.inner && yupError.inner.length > 0
81
+ ? yupError.inner
82
+ : [yupError];
83
+ return {
84
+ success: false,
85
+ errors: this.mapIssues(issues),
86
+ };
87
+ }
88
+ }
89
+ async transform(data, schema) {
90
+ if (typeof schema.cast === "function") {
91
+ try {
92
+ return schema.cast(data);
93
+ }
94
+ catch {
95
+ return data;
96
+ }
97
+ }
98
+ return data;
99
+ }
100
+ mapIssues(issues) {
101
+ return issues.map((issue) => ({
102
+ path: issue.path ?? "",
103
+ message: issue.message ?? "Validation failed",
104
+ code: issue.type ?? "validation_error",
105
+ received: issue.value,
106
+ }));
107
+ }
108
+ }
109
+ exports.YupValidatorAdapter = YupValidatorAdapter;
110
+ /**
111
+ * Convenience factory matching the `createXxxValidator` naming used by the
112
+ * other adapters. Equivalent to `new YupValidatorAdapter()`.
113
+ *
114
+ * @public API
115
+ */
116
+ function createYupValidator() {
117
+ return new YupValidatorAdapter();
118
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * Zod Validation Adapter
4
+ * @module @expressots/core/validation
5
+ *
6
+ * Built-in adapter for the [zod](https://zod.dev) validation library.
7
+ * `zod` is declared as an *optional* peer dependency: install it explicitly
8
+ * (`npm i zod`) to enable this adapter; otherwise validation falls back to
9
+ * the next registered adapter.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { z } from "zod";
14
+ * import { validationRegistry, ZodValidatorAdapter, createZodValidator } from "@expressots/core";
15
+ *
16
+ * validationRegistry.register(createZodValidator());
17
+ *
18
+ * const CreateUserSchema = z.object({
19
+ * email: z.string().email(),
20
+ * age: z.number().int().min(18),
21
+ * });
22
+ *
23
+ * @controller("/users")
24
+ * class UsersController {
25
+ * @Post("/")
26
+ * create(@body(CreateUserSchema) input: z.infer<typeof CreateUserSchema>) {
27
+ * // input is fully typed and validated
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.ZodValidatorAdapter = void 0;
34
+ exports.createZodValidator = createZodValidator;
35
+ /**
36
+ * Adapter for [zod](https://zod.dev). Picks up any zod schema (`z.object`,
37
+ * `z.string`, ...) at runtime by structural detection — no compile-time
38
+ * dependency on `zod` is required.
39
+ *
40
+ * @public API
41
+ */
42
+ class ZodValidatorAdapter {
43
+ constructor() {
44
+ this.name = "zod";
45
+ this.priority = 90;
46
+ }
47
+ canHandle(schema) {
48
+ if (schema === null || typeof schema !== "object")
49
+ return false;
50
+ const candidate = schema;
51
+ return (typeof candidate.safeParseAsync === "function" ||
52
+ typeof candidate.safeParse === "function" ||
53
+ typeof candidate.parseAsync === "function" ||
54
+ typeof candidate.parse === "function");
55
+ }
56
+ async validate(data, schema,
57
+ // ValidationOptions accepted for interface compliance; Zod surfaces its own
58
+ // configuration through the schema itself, so options are not consumed here.
59
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
60
+ _options) {
61
+ try {
62
+ // Prefer the safe variants because they encode the failure mode in the
63
+ // result rather than throwing.
64
+ if (typeof schema.safeParseAsync === "function") {
65
+ const r = await schema.safeParseAsync(data);
66
+ return r.success
67
+ ? { success: true, data: r.data }
68
+ : { success: false, errors: this.mapIssues(r.error?.issues ?? []) };
69
+ }
70
+ if (typeof schema.safeParse === "function") {
71
+ const r = schema.safeParse(data);
72
+ return r.success
73
+ ? { success: true, data: r.data }
74
+ : { success: false, errors: this.mapIssues(r.error?.issues ?? []) };
75
+ }
76
+ if (typeof schema.parseAsync === "function") {
77
+ const parsed = await schema.parseAsync(data);
78
+ return { success: true, data: parsed };
79
+ }
80
+ if (typeof schema.parse === "function") {
81
+ const parsed = schema.parse(data);
82
+ return { success: true, data: parsed };
83
+ }
84
+ return {
85
+ success: false,
86
+ errors: [
87
+ {
88
+ path: "",
89
+ message: "ZodValidatorAdapter received a schema with no parse/safeParse method.",
90
+ code: "invalid_schema",
91
+ },
92
+ ],
93
+ };
94
+ }
95
+ catch (error) {
96
+ // zod throws ZodError for `parse`/`parseAsync`. Pull issues if present.
97
+ const maybeIssues = error?.issues;
98
+ if (Array.isArray(maybeIssues)) {
99
+ return { success: false, errors: this.mapIssues(maybeIssues) };
100
+ }
101
+ return {
102
+ success: false,
103
+ errors: [
104
+ {
105
+ path: "",
106
+ message: error instanceof Error
107
+ ? error.message
108
+ : "Validation failed unexpectedly",
109
+ code: "validation_error",
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ }
115
+ async transform(data, schema) {
116
+ const result = await this.validate(data, schema);
117
+ return result.success ? result.data : data;
118
+ }
119
+ mapIssues(issues) {
120
+ return issues.map((issue) => ({
121
+ path: issue.path.join("."),
122
+ message: issue.message,
123
+ code: issue.code ?? "validation_error",
124
+ received: issue.received,
125
+ }));
126
+ }
127
+ }
128
+ exports.ZodValidatorAdapter = ZodValidatorAdapter;
129
+ /**
130
+ * Convenience factory matching the `createXxxValidator` naming used by the
131
+ * other adapters. Equivalent to `new ZodValidatorAdapter()`.
132
+ *
133
+ * @public API
134
+ */
135
+ function createZodValidator() {
136
+ return new ZodValidatorAdapter();
137
+ }
@@ -13,7 +13,7 @@
13
13
  * - Universal parameter validation (@body, @query, @param, @headers)
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.ClassValidatorAdapter = exports.detectSchemaType = exports.isClassConstructor = exports.isZodSchema = exports.hasClassValidatorDecorators = exports.getClassProperties = exports.getAllParameterTypes = exports.getParameterType = exports.HelpfulErrorFormatter = exports.SmartFieldDetector = exports.ValidationRegistry = void 0;
16
+ exports.createYupValidator = exports.YupValidatorAdapter = exports.createZodValidator = exports.ZodValidatorAdapter = exports.ClassValidatorAdapter = exports.detectSchemaType = exports.isClassConstructor = exports.isZodSchema = exports.hasClassValidatorDecorators = exports.getClassProperties = exports.getAllParameterTypes = exports.getParameterType = exports.HelpfulErrorFormatter = exports.SmartFieldDetector = exports.ValidationRegistry = void 0;
17
17
  // Core components
18
18
  var validation_registry_js_1 = require("./validation-registry.js");
19
19
  Object.defineProperty(exports, "ValidationRegistry", { enumerable: true, get: function () { return validation_registry_js_1.ValidationRegistry; } });
@@ -33,3 +33,7 @@ Object.defineProperty(exports, "detectSchemaType", { enumerable: true, get: func
33
33
  // Built-in adapters
34
34
  var index_js_1 = require("./adapters/index.js");
35
35
  Object.defineProperty(exports, "ClassValidatorAdapter", { enumerable: true, get: function () { return index_js_1.ClassValidatorAdapter; } });
36
+ Object.defineProperty(exports, "ZodValidatorAdapter", { enumerable: true, get: function () { return index_js_1.ZodValidatorAdapter; } });
37
+ Object.defineProperty(exports, "createZodValidator", { enumerable: true, get: function () { return index_js_1.createZodValidator; } });
38
+ Object.defineProperty(exports, "YupValidatorAdapter", { enumerable: true, get: function () { return index_js_1.YupValidatorAdapter; } });
39
+ Object.defineProperty(exports, "createYupValidator", { enumerable: true, get: function () { return index_js_1.createYupValidator; } });
@@ -300,20 +300,20 @@ class ReactAdapter extends base_adapter_js_1.BaseEngineAdapter {
300
300
  .replace(/</g, "\\u003c")
301
301
  .replace(/>/g, "\\u003e")
302
302
  .replace(/&/g, "\\u0026");
303
- return `<!DOCTYPE html>
304
- <html lang="en">
305
- <head>
306
- <meta charset="UTF-8">
307
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
308
- <script>
309
- window.__INITIAL_DATA__ = ${safeData};
310
- window.__COMPONENT__ = "${view}";
311
- </script>
312
- </head>
313
- <body>
314
- <div id="root">${html}</div>
315
- <script src="/assets/client.js" defer></script>
316
- </body>
303
+ return `<!DOCTYPE html>
304
+ <html lang="en">
305
+ <head>
306
+ <meta charset="UTF-8">
307
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
308
+ <script>
309
+ window.__INITIAL_DATA__ = ${safeData};
310
+ window.__COMPONENT__ = "${view}";
311
+ </script>
312
+ </head>
313
+ <body>
314
+ <div id="root">${html}</div>
315
+ <script src="/assets/client.js" defer></script>
316
+ </body>
317
317
  </html>`;
318
318
  }
319
319
  /**
@@ -114,36 +114,36 @@ class TypeGenerator {
114
114
  const propsType = v.propsType || "Record<string, unknown>";
115
115
  return ` "${v.name}": ${propsType};`;
116
116
  });
117
- return `/**
118
- * Auto-generated view types
119
- * Generated by ExpressoTS Render Engine
120
- * Do not edit this file manually
121
- */
122
-
123
- /**
124
- * View name to props type mapping
125
- */
126
- export interface Views {
127
- ${viewTypes.join("\n")}
128
- }
129
-
130
- /**
131
- * All view names
132
- */
133
- export type ViewName = keyof Views;
134
-
135
- /**
136
- * Get props type for a specific view
137
- */
138
- export type ViewProps<T extends ViewName> = Views[T];
139
-
140
- /**
141
- * Type-safe render function signature
142
- */
143
- export type RenderFunction = <T extends ViewName>(
144
- view: T,
145
- data: Views[T],
146
- ) => Promise<string>;
117
+ return `/**
118
+ * Auto-generated view types
119
+ * Generated by ExpressoTS Render Engine
120
+ * Do not edit this file manually
121
+ */
122
+
123
+ /**
124
+ * View name to props type mapping
125
+ */
126
+ export interface Views {
127
+ ${viewTypes.join("\n")}
128
+ }
129
+
130
+ /**
131
+ * All view names
132
+ */
133
+ export type ViewName = keyof Views;
134
+
135
+ /**
136
+ * Get props type for a specific view
137
+ */
138
+ export type ViewProps<T extends ViewName> = Views[T];
139
+
140
+ /**
141
+ * Type-safe render function signature
142
+ */
143
+ export type RenderFunction = <T extends ViewName>(
144
+ view: T,
145
+ data: Views[T],
146
+ ) => Promise<string>;
147
147
  `;
148
148
  }
149
149
  /**
@@ -29,12 +29,18 @@ class ViewDebugger {
29
29
  app.get("/__views", (req, res) => {
30
30
  this.handleViewsEndpoint(req, res);
31
31
  });
32
- // Preview a view
33
- app.get("/__views/preview/:view(*)", (req, res) => {
32
+ // Preview a view. The view name can contain slashes (e.g.
33
+ // `users/profile`), so we use a named splat (`*view`) the
34
+ // path-to-regexp v6 form `:view(*)` shipped by Express 4 is not
35
+ // valid in path-to-regexp v8 (Express 5) and now throws
36
+ // `Missing parameter name`. Path-to-regexp v8 captures the splat
37
+ // segments as `req.params.view: string[]`; the helpers below
38
+ // normalise that to a slash-joined string for backward compat.
39
+ app.get("/__views/preview/*view", (req, res) => {
34
40
  this.handlePreviewEndpoint(req, res);
35
41
  });
36
- // Get view info
37
- app.get("/__views/info/:view(*)", (req, res) => {
42
+ // Get view info — same shape as preview.
43
+ app.get("/__views/info/*view", (req, res) => {
38
44
  this.handleInfoEndpoint(req, res);
39
45
  });
40
46
  // Get metrics
@@ -73,7 +79,7 @@ class ViewDebugger {
73
79
  */
74
80
  async handlePreviewEndpoint(req, res) {
75
81
  try {
76
- const view = req.params.view;
82
+ const view = this.resolveViewParam(req.params.view);
77
83
  const data = req.query.data ? JSON.parse(req.query.data) : {};
78
84
  const html = await this.renderService.render(view, data);
79
85
  res.setHeader("Content-Type", "text/html");
@@ -82,16 +88,30 @@ class ViewDebugger {
82
88
  catch (error) {
83
89
  res.status(500).json({
84
90
  error: error.message,
85
- view: req.params.view,
91
+ view: this.resolveViewParam(req.params.view),
86
92
  });
87
93
  }
88
94
  }
95
+ /**
96
+ * Normalise the `view` route param across Express 4 and 5.
97
+ *
98
+ * Express 4 / path-to-regexp v6 captured `/__views/preview/:view(*)`
99
+ * as a single string. Express 5 / path-to-regexp v8 changed the
100
+ * named-splat capture to `string[]` (one entry per slash-separated
101
+ * segment). The view engine needs the slash-joined form, so we always
102
+ * collapse the array shape back to that.
103
+ */
104
+ resolveViewParam(value) {
105
+ if (Array.isArray(value))
106
+ return value.join("/");
107
+ return typeof value === "string" ? value : "";
108
+ }
89
109
  /**
90
110
  * Handle the view info endpoint.
91
111
  */
92
112
  handleInfoEndpoint(req, res) {
93
113
  try {
94
- const view = req.params.view;
114
+ const view = this.resolveViewParam(req.params.view);
95
115
  const viewFiles = this.renderService.getViewFiles();
96
116
  // Find matching view
97
117
  const matchingFiles = viewFiles.filter((file) => file.includes(view));
@@ -120,54 +140,54 @@ class ViewDebugger {
120
140
  * Wrap preview HTML with debug info.
121
141
  */
122
142
  wrapPreview(view, html) {
123
- return `<!DOCTYPE html>
124
- <html>
125
- <head>
126
- <title>View Preview: ${view}</title>
127
- <style>
128
- .debug-bar {
129
- position: fixed;
130
- top: 0;
131
- left: 0;
132
- right: 0;
133
- background: #1a1a2e;
134
- color: #eee;
135
- padding: 8px 16px;
136
- font-family: system-ui, -apple-system, sans-serif;
137
- font-size: 13px;
138
- z-index: 99999;
139
- display: flex;
140
- align-items: center;
141
- gap: 16px;
142
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
143
- }
144
- .debug-bar strong {
145
- color: #00d9ff;
146
- }
147
- .debug-bar a {
148
- color: #ff6b6b;
149
- text-decoration: none;
150
- }
151
- .debug-bar a:hover {
152
- text-decoration: underline;
153
- }
154
- .content-wrapper {
155
- padding-top: 50px;
156
- }
157
- </style>
158
- </head>
159
- <body>
160
- <div class="debug-bar">
161
- <span>📁 <strong>${view}</strong></span>
162
- <span>|</span>
163
- <span>Engine: <strong>${this.renderService.getActiveEngine().name}</strong></span>
164
- <span>|</span>
165
- <a href="/__views">← Back to Views</a>
166
- </div>
167
- <div class="content-wrapper">
168
- ${html}
169
- </div>
170
- </body>
143
+ return `<!DOCTYPE html>
144
+ <html>
145
+ <head>
146
+ <title>View Preview: ${view}</title>
147
+ <style>
148
+ .debug-bar {
149
+ position: fixed;
150
+ top: 0;
151
+ left: 0;
152
+ right: 0;
153
+ background: #1a1a2e;
154
+ color: #eee;
155
+ padding: 8px 16px;
156
+ font-family: system-ui, -apple-system, sans-serif;
157
+ font-size: 13px;
158
+ z-index: 99999;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 16px;
162
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
163
+ }
164
+ .debug-bar strong {
165
+ color: #00d9ff;
166
+ }
167
+ .debug-bar a {
168
+ color: #ff6b6b;
169
+ text-decoration: none;
170
+ }
171
+ .debug-bar a:hover {
172
+ text-decoration: underline;
173
+ }
174
+ .content-wrapper {
175
+ padding-top: 50px;
176
+ }
177
+ </style>
178
+ </head>
179
+ <body>
180
+ <div class="debug-bar">
181
+ <span>📁 <strong>${view}</strong></span>
182
+ <span>|</span>
183
+ <span>Engine: <strong>${this.renderService.getActiveEngine().name}</strong></span>
184
+ <span>|</span>
185
+ <a href="/__views">← Back to Views</a>
186
+ </div>
187
+ <div class="content-wrapper">
188
+ ${html}
189
+ </div>
190
+ </body>
171
191
  </html>`;
172
192
  }
173
193
  }
@@ -233,7 +233,13 @@ class FluentRequestImpl {
233
233
  }
234
234
  /**
235
235
  * Execute the request and return the response.
236
+ *
237
+ * The generic defaults to `any` (see {@link FluentResponse} for the
238
+ * rationale) so test code can read `response.body.foo` directly. Pass
239
+ * an explicit type — `.execute<UserDto>()` — to opt into strict
240
+ * typing.
236
241
  */
242
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
237
243
  async execute() {
238
244
  const startTime = Date.now();
239
245
  // Build URL with query parameters
@@ -317,6 +323,7 @@ class FluentRequestImpl {
317
323
  /**
318
324
  * Alias for execute().
319
325
  */
326
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
327
  end() {
321
328
  return this.execute();
322
329
  }
@@ -120,9 +120,11 @@ class SnapshotRequestImpl {
120
120
  this.fluentRequest.expect(assertion);
121
121
  return this;
122
122
  }
123
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
124
  execute() {
124
125
  return this.fluentRequest.execute();
125
126
  }
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
128
  end() {
127
129
  return this.fluentRequest.end();
128
130
  }
@@ -112,6 +112,12 @@ export declare class AppFactory {
112
112
  *
113
113
  * @throws {Error} If webServerType is not a valid constructor
114
114
  *
115
+ * @deprecated Use `bootstrap()` from `@expressots/core` instead. `bootstrap()`
116
+ * additionally handles environment file loading, port configuration,
117
+ * graceful shutdown, the startup banner, and configuration validation.
118
+ * `AppFactory.create()` will keep working for advanced use cases but is
119
+ * scheduled for removal in a future major release.
120
+ *
115
121
  * @public API
116
122
  */
117
123
  static create<T extends IWebServer>(webServerType: IWebServerConstructor<T>): Promise<IWebServerBuilder>;