millas 0.2.12-beta → 0.2.12-beta-2

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 (116) hide show
  1. package/package.json +3 -16
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +31 -10
  31. package/src/auth/AuthController.js +3 -1
  32. package/src/auth/AuthUser.js +119 -0
  33. package/src/cli.js +4 -2
  34. package/src/commands/createsuperuser.js +254 -0
  35. package/src/commands/lang.js +589 -0
  36. package/src/commands/migrate.js +154 -81
  37. package/src/commands/serve.js +82 -110
  38. package/src/container/AppInitializer.js +215 -0
  39. package/src/container/Application.js +278 -253
  40. package/src/container/HttpServer.js +156 -0
  41. package/src/container/MillasApp.js +29 -279
  42. package/src/container/MillasConfig.js +192 -0
  43. package/src/core/admin.js +5 -0
  44. package/src/core/auth.js +9 -0
  45. package/src/core/db.js +9 -0
  46. package/src/core/foundation.js +59 -0
  47. package/src/core/http.js +11 -0
  48. package/src/core/lang.js +1 -0
  49. package/src/core/mail.js +6 -0
  50. package/src/core/queue.js +7 -0
  51. package/src/core/validation.js +29 -0
  52. package/src/facades/Admin.js +1 -1
  53. package/src/facades/Auth.js +22 -39
  54. package/src/facades/Cache.js +21 -10
  55. package/src/facades/Database.js +1 -1
  56. package/src/facades/Events.js +18 -17
  57. package/src/facades/Facade.js +197 -0
  58. package/src/facades/Http.js +42 -45
  59. package/src/facades/Log.js +25 -49
  60. package/src/facades/Mail.js +27 -32
  61. package/src/facades/Queue.js +22 -15
  62. package/src/facades/Storage.js +18 -10
  63. package/src/facades/Url.js +53 -0
  64. package/src/http/HttpClient.js +673 -0
  65. package/src/http/ResponseDispatcher.js +18 -111
  66. package/src/http/UrlGenerator.js +375 -0
  67. package/src/http/WelcomePage.js +273 -0
  68. package/src/http/adapters/ExpressAdapter.js +315 -0
  69. package/src/http/adapters/HttpAdapter.js +168 -0
  70. package/src/http/adapters/index.js +9 -0
  71. package/src/i18n/I18nServiceProvider.js +91 -0
  72. package/src/i18n/Translator.js +635 -0
  73. package/src/i18n/defaults.js +122 -0
  74. package/src/i18n/index.js +164 -0
  75. package/src/i18n/locales/en.js +55 -0
  76. package/src/i18n/locales/sw.js +48 -0
  77. package/src/index.js +5 -144
  78. package/src/logger/formatters/PrettyFormatter.js +103 -57
  79. package/src/logger/internal.js +2 -2
  80. package/src/logger/patchConsole.js +91 -81
  81. package/src/middleware/MiddlewareRegistry.js +62 -82
  82. package/src/migrations/system/0001_users.js +21 -0
  83. package/src/migrations/system/0002_admin_log.js +25 -0
  84. package/src/migrations/system/0003_sessions.js +23 -0
  85. package/src/orm/fields/index.js +210 -188
  86. package/src/orm/migration/DefaultValueParser.js +325 -0
  87. package/src/orm/migration/InteractiveResolver.js +191 -0
  88. package/src/orm/migration/Makemigrations.js +312 -0
  89. package/src/orm/migration/MigrationGraph.js +227 -0
  90. package/src/orm/migration/MigrationRunner.js +202 -108
  91. package/src/orm/migration/MigrationWriter.js +463 -0
  92. package/src/orm/migration/ModelInspector.js +412 -344
  93. package/src/orm/migration/ModelScanner.js +225 -0
  94. package/src/orm/migration/ProjectState.js +213 -0
  95. package/src/orm/migration/RenameDetector.js +175 -0
  96. package/src/orm/migration/SchemaBuilder.js +8 -81
  97. package/src/orm/migration/operations/base.js +57 -0
  98. package/src/orm/migration/operations/column.js +191 -0
  99. package/src/orm/migration/operations/fields.js +252 -0
  100. package/src/orm/migration/operations/index.js +55 -0
  101. package/src/orm/migration/operations/models.js +152 -0
  102. package/src/orm/migration/operations/registry.js +131 -0
  103. package/src/orm/migration/operations/special.js +51 -0
  104. package/src/orm/migration/utils.js +208 -0
  105. package/src/orm/model/Model.js +81 -13
  106. package/src/providers/AdminServiceProvider.js +66 -9
  107. package/src/providers/AuthServiceProvider.js +46 -7
  108. package/src/providers/CacheStorageServiceProvider.js +5 -3
  109. package/src/providers/DatabaseServiceProvider.js +3 -2
  110. package/src/providers/EventServiceProvider.js +2 -1
  111. package/src/providers/LogServiceProvider.js +7 -3
  112. package/src/providers/MailServiceProvider.js +4 -3
  113. package/src/providers/QueueServiceProvider.js +4 -3
  114. package/src/router/Router.js +119 -152
  115. package/src/scaffold/templates.js +83 -26
  116. package/src/facades/Validation.js +0 -69
@@ -0,0 +1,273 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WelcomePage
5
+ *
6
+ * Renders a branded welcome page when no GET / route is defined.
7
+ * Automatically removed once the developer registers their own / route.
8
+ *
9
+ * Only shown to browser requests (Accept: text/html).
10
+ * API clients always receive JSON.
11
+ */
12
+ class WelcomePage {
13
+
14
+ /**
15
+ * Returns an Express middleware that serves the welcome page for GET /
16
+ * only when no user-defined route has been registered for that path.
17
+ *
18
+ * @param {string} version — package version shown in the page
19
+ */
20
+ static handler(version = '') {
21
+ return function millaWelcome(req, res) {
22
+ const accept = req.headers?.accept || '';
23
+ const wantsHtml = accept.includes('text/html') && !accept.startsWith('application/json');
24
+
25
+ if (!wantsHtml) {
26
+ return res.json({
27
+ framework: 'Millas',
28
+ version,
29
+ message: 'Welcome to your Millas app. Define your routes in routes/api.js.',
30
+ docs: 'https://millas.dev/docs',
31
+ });
32
+ }
33
+
34
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
35
+ res.send(WelcomePage._render(version));
36
+ };
37
+ }
38
+
39
+ static _render(version) {
40
+ const ver = version ? `v${version}` : '';
41
+ return `<!DOCTYPE html>
42
+ <html lang="en">
43
+ <head>
44
+ <meta charset="UTF-8">
45
+ <meta name="viewport" content="width=device-width, initial-scale=1">
46
+ <title>Welcome — Millas</title>
47
+ <style>
48
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
49
+
50
+ :root {
51
+ --orange: #f97316;
52
+ --orange-dark: #ea580c;
53
+ --orange-dim: #fff7ed;
54
+ --orange-mid: #fed7aa;
55
+ --bg: #fafafa;
56
+ --surface: #ffffff;
57
+ --border: #e5e7eb;
58
+ --text: #111827;
59
+ --muted: #6b7280;
60
+ --code-bg: #fff7ed;
61
+ --code-fg: #c2410c;
62
+ }
63
+
64
+ @media (prefers-color-scheme: dark) {
65
+ :root {
66
+ --orange: #fb923c;
67
+ --orange-dark: #f97316;
68
+ --orange-dim: #1c0f00;
69
+ --orange-mid: #7c2d12;
70
+ --bg: #0c0c0c;
71
+ --surface: #141414;
72
+ --border: #262626;
73
+ --text: #f5f5f5;
74
+ --muted: #737373;
75
+ --code-bg: #1a0e00;
76
+ --code-fg: #fb923c;
77
+ }
78
+ }
79
+
80
+ body {
81
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
82
+ background: var(--bg);
83
+ color: var(--text);
84
+ min-height: 100vh;
85
+ display: flex;
86
+ flex-direction: column;
87
+ align-items: center;
88
+ justify-content: center;
89
+ padding: 40px 20px;
90
+ }
91
+
92
+ /* ── Logo mark ── */
93
+ .logo {
94
+ width: 64px;
95
+ height: 64px;
96
+ background: linear-gradient(135deg, var(--orange), var(--orange-dark));
97
+ border-radius: 18px;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ margin-bottom: 28px;
102
+ box-shadow: 0 8px 32px color-mix(in srgb, var(--orange) 35%, transparent);
103
+ }
104
+ .logo svg { width: 34px; height: 34px; }
105
+
106
+ /* ── Card ── */
107
+ .card {
108
+ padding: 48px 52px;
109
+ max-width: 560px;
110
+ width: 100%;
111
+ text-align: center;
112
+ }
113
+
114
+ h1 {
115
+ font-size: 28px;
116
+ font-weight: 800;
117
+ letter-spacing: -.5px;
118
+ color: var(--text);
119
+ line-height: 1.2;
120
+ }
121
+ h1 span { color: var(--orange); }
122
+
123
+ .version {
124
+ display: inline-block;
125
+ margin-top: 10px;
126
+ background: var(--orange-dim);
127
+ color: var(--orange-dark);
128
+ border: 1px solid var(--orange-mid);
129
+ border-radius: 99px;
130
+ font-size: 12px;
131
+ font-weight: 600;
132
+ padding: 3px 12px;
133
+ font-family: 'Cascadia Code', 'Fira Code', monospace;
134
+ }
135
+
136
+ .tagline {
137
+ margin-top: 16px;
138
+ font-size: 15px;
139
+ color: var(--muted);
140
+ line-height: 1.6;
141
+ }
142
+
143
+ /* ── Divider ── */
144
+ .divider {
145
+ height: 1px;
146
+ background: var(--border);
147
+ margin: 32px 0;
148
+ }
149
+
150
+ /* ── Quickstart block ── */
151
+ .qs-label {
152
+ font-size: 11px;
153
+ font-weight: 700;
154
+ text-transform: uppercase;
155
+ letter-spacing: .8px;
156
+ color: var(--muted);
157
+ margin-bottom: 12px;
158
+ }
159
+
160
+ .code-block {
161
+ background: var(--code-bg);
162
+ border: 1px solid var(--orange-mid);
163
+ border-radius: 12px;
164
+ padding: 18px 20px;
165
+ text-align: left;
166
+ font-family: 'Cascadia Code', 'Fira Code', 'SF Mono', monospace;
167
+ font-size: 13px;
168
+ line-height: 1.9;
169
+ color: var(--code-fg);
170
+ }
171
+ .code-block .comment { color: var(--muted); font-style: italic; }
172
+ .code-block .kw { color: var(--orange-dark); font-weight: 700; }
173
+ .code-block .str { color: #16a34a; }
174
+
175
+ /* ── Links row ── */
176
+ .links {
177
+ margin-top: 28px;
178
+ display: flex;
179
+ justify-content: center;
180
+ gap: 12px;
181
+ flex-wrap: wrap;
182
+ }
183
+ .link {
184
+ display: inline-flex;
185
+ align-items: center;
186
+ gap: 6px;
187
+ padding: 8px 18px;
188
+ border-radius: 99px;
189
+ font-size: 13px;
190
+ font-weight: 600;
191
+ text-decoration: none;
192
+ transition: opacity .15s;
193
+ }
194
+ .link:hover { opacity: .8; }
195
+ .link-primary {
196
+ background: linear-gradient(135deg, var(--orange), var(--orange-dark));
197
+ color: #fff;
198
+ }
199
+ .link-ghost {
200
+ background: var(--surface);
201
+ color: var(--text);
202
+ border: 1px solid var(--border);
203
+ }
204
+
205
+ /* ── Footer note ── */
206
+ .note {
207
+ margin-top: 36px;
208
+ font-size: 12px;
209
+ color: var(--muted);
210
+ line-height: 1.6;
211
+ }
212
+ .note strong { color: var(--orange); font-family: monospace; font-weight: 600; }
213
+ </style>
214
+ </head>
215
+ <body>
216
+
217
+ <div class="logo">
218
+ <!-- M lettermark -->
219
+ <svg viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
220
+ <path d="M4 27V7l13 13L30 7v20" stroke="#fff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
221
+ </svg>
222
+ </div>
223
+
224
+ <div class="card">
225
+ <h1>Welcome to <span>Millas</span></h1>
226
+ ${ver ? `<div class="version">${_esc(ver)}</div>` : ''}
227
+
228
+ <p class="tagline">
229
+ Your app is running. Define your first route and this page will disappear.
230
+ </p>
231
+
232
+ <div class="divider"></div>
233
+
234
+ <div class="qs-label">Get started</div>
235
+ <div class="code-block">
236
+ <span class="comment">// routes/api.js</span>
237
+ <span class="kw">module</span>.exports = <span class="kw">function</span> (Route) {
238
+ Route.get(<span class="str">'/'</span>, () => ({
239
+ message: <span class="str">'Hello from Millas!'</span>
240
+ }));
241
+ };
242
+ </div>
243
+
244
+ <div class="links">
245
+ <a class="link link-primary" href="https://id-preview--ae4ed87b-a9d5-434d-8559-1e8c30972a28.lovable.app" target="_blank">
246
+ 📖 Documentation
247
+ </a>
248
+ <a class="link link-ghost" href="https://github.com/millas-framework/millas" target="_blank">
249
+ GitHub
250
+ </a>
251
+ </div>
252
+ </div>
253
+
254
+ <p class="note">
255
+ This page is shown because no route is registered for <strong>GET /</strong>.<br>
256
+ It will never appear in production to end users.
257
+ </p>
258
+
259
+ </body>
260
+ </html>`;
261
+ }
262
+ }
263
+
264
+ function _esc(str) {
265
+ return String(str ?? '')
266
+ .replace(/&/g, '&amp;')
267
+ .replace(/</g, '&lt;')
268
+ .replace(/>/g, '&gt;')
269
+ .replace(/"/g, '&quot;')
270
+ .replace(/'/g, '&#39;');
271
+ }
272
+
273
+ module.exports = WelcomePage;
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+
3
+ const HttpAdapter = require('./HttpAdapter');
4
+ const MillasRequest = require('../MillasRequest');
5
+ const MillasResponse = require('../MillasResponse');
6
+ const RequestContext = require('../RequestContext');
7
+ const ErrorRenderer = require('../../errors/ErrorRenderer');
8
+ const WelcomePage = require('../WelcomePage');
9
+ const http = require("http");
10
+
11
+ /**
12
+ * ExpressAdapter
13
+ *
14
+ * The Express implementation of HttpAdapter.
15
+ * This is the ONLY file in the entire Millas codebase that imports Express
16
+ * or calls Express APIs (req, res, next, app[verb], app.use, etc.).
17
+ *
18
+ * Swapping to a different HTTP engine means writing a new adapter class —
19
+ * zero changes to the kernel, Router, MiddlewareRegistry, or any user code.
20
+ */
21
+ class ExpressAdapter extends HttpAdapter {
22
+
23
+ /**
24
+ * @param {import('express').Application} expressApp
25
+ */
26
+ constructor(expressApp) {
27
+ super();
28
+ this._app = expressApp;
29
+ this._server = null;
30
+ }
31
+
32
+ // ── Setup ──────────────────────────────────────────────────────────────────
33
+
34
+ applyBodyParsers() {
35
+ const express = require('express');
36
+ this._app.use(express.json());
37
+ this._app.use(express.urlencoded({extended: true}));
38
+ }
39
+
40
+ applyMiddleware(fn) {
41
+ this._app.use(fn);
42
+ }
43
+
44
+ // ── Route mounting ─────────────────────────────────────────────────────────
45
+
46
+ mountRoute(verb, path, handlers) {
47
+ this._app[verb.toLowerCase()](path, ...handlers);
48
+ }
49
+
50
+ mountWelcome(handler) {
51
+ this._app.get('/', handler);
52
+ }
53
+
54
+ mountNotFound() {
55
+ this._app.use(ErrorRenderer.notFound());
56
+ }
57
+
58
+ mountErrorHandler() {
59
+ this._app.use(ErrorRenderer.handler());
60
+ }
61
+
62
+ // ── Request / Response bridge ──────────────────────────────────────────────
63
+
64
+ /**
65
+ * Wrap a Millas kernel handler into an Express (req, res, next) function.
66
+ *
67
+ * This is the ONLY place in the codebase where Express req/res/next
68
+ * appear outside of adapter code. The kernel handler never sees them.
69
+ */
70
+ wrapKernelHandler(kernelFn, displayName, container) {
71
+ const fnName = displayName || kernelFn.name || 'anonymous';
72
+
73
+ return (expressReq, expressRes, expressNext) => {
74
+ const millaReq = new MillasRequest(expressReq);
75
+ const ctx = new RequestContext(millaReq, container);
76
+
77
+ let nextCalled = false;
78
+ const trackedNext = (...args) => {
79
+ nextCalled = true;
80
+ expressNext(...args);
81
+ };
82
+
83
+ new Promise((resolve, reject) => {
84
+ try {
85
+ resolve(kernelFn(ctx, trackedNext));
86
+ } catch (err) {
87
+ reject(err);
88
+ }
89
+ })
90
+ .then(value => {
91
+ if (nextCalled) return;
92
+ if (expressRes.headersSent) return;
93
+
94
+ if (value === undefined || value === null) {
95
+ return expressNext(Object.assign(
96
+ new Error(
97
+ `Route handler "${fnName}" did not return a response.\n` +
98
+ `Return a MillasResponse or a plain value:\n\n` +
99
+ ` return jsonify({ ok: true })\n` +
100
+ ` return { ok: true } // auto-wrapped\n` +
101
+ ` return 'Hello world' // auto-wrapped\n` +
102
+ ` return redirect('/login')\n` +
103
+ ` return view('home', { data })`
104
+ ),
105
+ {status: 500, statusCode: 500}
106
+ ));
107
+ }
108
+
109
+ if (value instanceof Error) return expressNext(value);
110
+
111
+ try {
112
+ const response = MillasResponse.isResponse(value)
113
+ ? value
114
+ : this._autoWrap(value);
115
+ this.dispatch(response, expressRes);
116
+ } catch (dispatchErr) {
117
+ expressNext(dispatchErr);
118
+ }
119
+ })
120
+ .catch(expressNext);
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Wrap a Millas middleware instance into an Express (req, res, next) function.
126
+ */
127
+ wrapMiddleware(instance, container) {
128
+ return (expressReq, expressRes, expressNext) => {
129
+ const millaReq = new MillasRequest(expressReq);
130
+ const ctx = new RequestContext(millaReq, container);
131
+
132
+ const next = () => {
133
+ expressNext();
134
+ return undefined;
135
+ };
136
+
137
+ new Promise((resolve, reject) => {
138
+ try {
139
+ resolve(instance.handle(ctx, next));
140
+ } catch (err) {
141
+ reject(err);
142
+ }
143
+ })
144
+ .then(value => {
145
+ if (value !== undefined && value !== null && !expressRes.headersSent) {
146
+ const response = MillasResponse.isResponse(value)
147
+ ? value
148
+ : this._autoWrap(value);
149
+ this.dispatch(response, expressRes);
150
+ }
151
+ })
152
+ .catch(expressNext);
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Dispatch a MillasResponse to the Express res object.
158
+ * This is the ONLY place in the codebase where Express res methods are called.
159
+ */
160
+ dispatch(response, expressRes) {
161
+ if (!response || !MillasResponse.isResponse(response)) {
162
+ throw new Error(
163
+ '[ExpressAdapter] Expected a MillasResponse. Got: ' + typeof response
164
+ );
165
+ }
166
+
167
+ // Status
168
+ expressRes.status(response.statusCode);
169
+
170
+ // CORS headers stored by CorsMiddleware on next() calls
171
+ const corsHeaders = expressRes.req?._corsHeaders;
172
+ if (corsHeaders) {
173
+ for (const [name, value] of Object.entries(corsHeaders)) {
174
+ expressRes.setHeader(name, value);
175
+ }
176
+ }
177
+
178
+ // Response headers
179
+ for (const [name, value] of Object.entries(response.headers)) {
180
+ expressRes.setHeader(name, value);
181
+ }
182
+
183
+ // Cookies
184
+ for (const [name, {value, options}] of Object.entries(response.cookies)) {
185
+ if (options.maxAge === 0 || options.expires?.getTime() === 0) {
186
+ expressRes.clearCookie(name, options);
187
+ } else {
188
+ expressRes.cookie(name, value, options);
189
+ }
190
+ }
191
+
192
+ // Body
193
+ const {type, body} = response;
194
+
195
+ switch (type) {
196
+ case 'json':
197
+ return expressRes.json(body);
198
+
199
+ case 'html':
200
+ case 'text':
201
+ return expressRes.send(body);
202
+
203
+ case 'redirect':
204
+ return expressRes.redirect(response.statusCode, body);
205
+
206
+ case 'empty':
207
+ return expressRes.end();
208
+
209
+ case 'file': {
210
+ const {path: filePath, download, name: fileName} = body;
211
+ if (download) {
212
+ return expressRes.download(
213
+ filePath,
214
+ fileName || require('path').basename(filePath)
215
+ );
216
+ }
217
+ return expressRes.sendFile(require('path').resolve(filePath));
218
+ }
219
+
220
+ case 'view': {
221
+ const {template, data} = body;
222
+ return expressRes.render(template, data);
223
+ }
224
+
225
+ case 'stream': {
226
+ if (body && typeof body.pipe === 'function') {
227
+ body.pipe(expressRes);
228
+ } else {
229
+ expressRes.end();
230
+ }
231
+ return;
232
+ }
233
+
234
+ default:
235
+ return expressRes.send(body);
236
+ }
237
+ }
238
+
239
+ // ── Server lifecycle ───────────────────────────────────────────────────────
240
+
241
+ listen(port, host) {
242
+ return new Promise((resolve, reject) => {
243
+ this.initListener(port, host, resolve, reject);
244
+ });
245
+ }
246
+
247
+ initListener(port, host, onListening, onError) {
248
+ const server = http.createServer(this._app);
249
+ server.listen(port, host);
250
+ server.on('error', onError);
251
+ server.on('listening', onListening);
252
+ }
253
+
254
+ close() {
255
+ return new Promise((resolve) => {
256
+ if (!this._server) return resolve();
257
+ this._server.close(() => resolve());
258
+ });
259
+ }
260
+
261
+ // ── WelcomePage factory ───────────────────────────────────────────────────
262
+
263
+ /**
264
+ * Build the welcome page native handler.
265
+ * Returned value is passed directly to mountWelcome().
266
+ */
267
+ makeWelcomeHandler(version) {
268
+ return WelcomePage.handler(version);
269
+ }
270
+
271
+ // ── Internal ──────────────────────────────────────────────────────────────
272
+
273
+ /**
274
+ * Auto-wrap a plain JS value into a MillasResponse.
275
+ * Kept inside the adapter — not on the kernel — because it ultimately
276
+ * decides what to send over the wire, which is adapter territory.
277
+ */
278
+ _autoWrap(value) {
279
+ if (MillasResponse.isResponse(value)) return value;
280
+ if (value instanceof Error) throw value;
281
+
282
+ if (typeof value === 'string') {
283
+ return value.trimStart().startsWith('<')
284
+ ? MillasResponse.html(value)
285
+ : MillasResponse.text(value);
286
+ }
287
+
288
+ if (
289
+ typeof value === 'object' ||
290
+ typeof value === 'number' ||
291
+ typeof value === 'boolean'
292
+ ) {
293
+ return MillasResponse.json(value);
294
+ }
295
+
296
+ return MillasResponse.text(String(value));
297
+ }
298
+
299
+ /**
300
+ * Expose the raw Express app for escape hatches.
301
+ * Prefer not using this — it couples code to Express.
302
+ */
303
+ get nativeApp() {
304
+ return this._app;
305
+ }
306
+
307
+ /**
308
+ * Expose the raw net.Server once listen() has been called.
309
+ */
310
+ get server() {
311
+ return this._server;
312
+ }
313
+ }
314
+
315
+ module.exports = ExpressAdapter;