millas 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "millas",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -11,7 +11,7 @@ module.exports = function (program) {
11
11
  .description('Detect model changes and generate migration files')
12
12
  .action(async () => {
13
13
  const ctx = getProjectContext();
14
- const { ModelInspector } = require('../orm/migration/ModelInspector');
14
+ const ModelInspector = require('../orm/migration/ModelInspector');
15
15
  const inspector = new ModelInspector(
16
16
  ctx.modelsPath,
17
17
  ctx.migrationsPath,
@@ -6,445 +6,151 @@ const MiddlewareRegistry = require('./MiddlewareRegistry');
6
6
  const WELCOME_PAGE = `<!DOCTYPE html>
7
7
  <html lang="en">
8
8
  <head>
9
- <meta charset="UTF-8">
10
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
- <title>Millas — Installation successful!</title>
12
- <style>
13
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
14
-
15
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
16
-
17
- :root {
18
- --bg: #080a12;
19
- --surface: #0f1220;
20
- --surface2: #161929;
21
- --border: #1e2235;
22
- --border2: #252840;
23
- --primary: #6366f1;
24
- --primary-h: #818cf8;
25
- --glow: rgba(99,102,241,0.15);
26
- --text: #e2e8f0;
27
- --muted: #475569;
28
- --soft: #94a3b8;
29
- --green: #22c55e;
30
- --yellow: #f59e0b;
31
- }
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
+
12
+ <title>Welcome to Millas</title>
13
+
14
+ <link rel="stylesheet"
15
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
16
+ crossorigin="anonymous" referrerpolicy="no-referrer"/>
17
+
18
+ <style>
19
+
20
+ body{
21
+ margin:0;
22
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
23
+ background:#fff;
24
+ color:#333;
25
+ display:flex;
26
+ align-items:center;
27
+ justify-content:center;
28
+ height:100vh;
29
+ text-align:center;
30
+ }
32
31
 
33
- html { scroll-behavior: smooth; }
32
+ .container{
33
+ max-width:720px;
34
+ padding:40px;
35
+ }
34
36
 
35
- body {
36
- font-family: 'Inter', system-ui, sans-serif;
37
- background: var(--bg);
38
- color: var(--text);
39
- min-height: 100vh;
40
- line-height: 1.6;
41
- overflow-x: hidden;
42
- }
37
+ .icon{
38
+ margin-bottom:20px;
39
+ }
43
40
 
44
- /* ── Animated background grid ── */
45
- body::before {
46
- content: '';
47
- position: fixed;
48
- inset: 0;
49
- background-image:
50
- linear-gradient(rgba(99,102,241,0.03) 1px, transparent 1px),
51
- linear-gradient(90deg, rgba(99,102,241,0.03) 1px, transparent 1px);
52
- background-size: 40px 40px;
53
- pointer-events: none;
54
- z-index: 0;
55
- }
41
+ h1{
42
+ font-size:32px;
43
+ margin-bottom:12px;
44
+ }
56
45
 
57
- /* ── Glow orbs ── */
58
- .orb {
59
- position: fixed;
60
- border-radius: 50%;
61
- filter: blur(80px);
62
- pointer-events: none;
63
- z-index: 0;
64
- opacity: 0.5;
65
- }
66
- .orb-1 { width: 500px; height: 500px; background: radial-gradient(circle, rgba(99,102,241,0.15), transparent 70%); top: -100px; left: -100px; animation: drift1 18s ease-in-out infinite; }
67
- .orb-2 { width: 400px; height: 400px; background: radial-gradient(circle, rgba(168,85,247,0.1), transparent 70%); bottom: -80px; right: -80px; animation: drift2 22s ease-in-out infinite; }
68
- @keyframes drift1 { 0%,100%{transform:translate(0,0)} 50%{transform:translate(60px,40px)} }
69
- @keyframes drift2 { 0%,100%{transform:translate(0,0)} 50%{transform:translate(-40px,-60px)} }
70
-
71
- /* ── Layout ── */
72
- .page { position: relative; z-index: 1; max-width: 900px; margin: 0 auto; padding: 60px 24px 80px; }
73
-
74
- /* ── Hero ── */
75
- .hero { text-align: center; margin-bottom: 64px; }
76
-
77
- .logo-wrap {
78
- display: inline-flex; align-items: center; justify-content: center;
79
- width: 80px; height: 80px; border-radius: 22px;
80
- background: linear-gradient(135deg, #6366f1, #a855f7);
81
- font-size: 36px; margin-bottom: 28px;
82
- box-shadow: 0 0 40px rgba(99,102,241,0.3), 0 0 80px rgba(99,102,241,0.1);
83
- animation: pulse 3s ease-in-out infinite;
84
- }
85
- @keyframes pulse { 0%,100%{box-shadow:0 0 40px rgba(99,102,241,0.3),0 0 80px rgba(99,102,241,0.1)} 50%{box-shadow:0 0 60px rgba(99,102,241,0.5),0 0 120px rgba(99,102,241,0.2)} }
86
-
87
- .version-badge {
88
- display: inline-flex; align-items: center; gap: 6px;
89
- background: var(--surface2); border: 1px solid var(--border2);
90
- color: var(--primary-h); border-radius: 99px;
91
- padding: 4px 14px; font-size: 12px; font-weight: 600;
92
- margin-bottom: 24px; letter-spacing: 0.3px;
93
- }
94
- .version-dot { width: 6px; height: 6px; background: var(--green); border-radius: 50%; animation: blink 2s ease-in-out infinite; }
95
- @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
96
-
97
- h1 {
98
- font-size: clamp(36px, 6vw, 56px);
99
- font-weight: 700;
100
- letter-spacing: -1.5px;
101
- line-height: 1.1;
102
- margin-bottom: 20px;
103
- background: linear-gradient(135deg, #fff 0%, #94a3b8 100%);
104
- -webkit-background-clip: text;
105
- -webkit-text-fill-color: transparent;
106
- background-clip: text;
107
- }
108
- h1 span {
109
- background: linear-gradient(135deg, #6366f1, #a855f7);
110
- -webkit-background-clip: text;
111
- -webkit-text-fill-color: transparent;
112
- background-clip: text;
113
- }
46
+ .subtitle{
47
+ color:#555;
48
+ font-size:16px;
49
+ line-height:1.7;
50
+ margin-bottom:25px;
51
+ }
114
52
 
115
- .hero-sub {
116
- font-size: 17px;
117
- color: var(--soft);
118
- max-width: 520px;
119
- margin: 0 auto 36px;
120
- font-weight: 400;
121
- }
53
+ code{
54
+ background:#fff4ed;
55
+ color:#ea580c;
56
+ padding:4px 8px;
57
+ border-radius:6px;
58
+ font-size:13px;
59
+ }
122
60
 
123
- .hero-actions {
124
- display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;
125
- }
126
- .btn {
127
- display: inline-flex; align-items: center; gap: 8px;
128
- padding: 11px 22px; border-radius: 10px;
129
- font-size: 14px; font-weight: 600;
130
- text-decoration: none; transition: all .2s;
131
- font-family: inherit; cursor: pointer; border: none;
132
- }
133
- .btn-primary {
134
- background: linear-gradient(135deg, #6366f1, #7c3aed);
135
- color: #fff;
136
- box-shadow: 0 4px 20px rgba(99,102,241,0.3);
137
- }
138
- .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(99,102,241,0.4); }
139
- .btn-ghost {
140
- background: var(--surface); color: var(--soft);
141
- border: 1px solid var(--border2);
142
- }
143
- .btn-ghost:hover { background: var(--surface2); color: var(--text); border-color: var(--primary); }
61
+ .steps{
62
+ margin-top:30px;
63
+ text-align:left;
64
+ display:inline-block;
65
+ }
144
66
 
145
- /* ── Divider ── */
146
- .divider {
147
- text-align: center; position: relative; margin: 48px 0;
148
- color: var(--muted); font-size: 12px; text-transform: uppercase;
149
- letter-spacing: 1px; font-weight: 600;
150
- }
151
- .divider::before {
152
- content: '';
153
- position: absolute; top: 50%; left: 0; right: 0;
154
- height: 1px; background: var(--border);
155
- }
156
- .divider span { background: var(--bg); padding: 0 16px; position: relative; }
157
-
158
- /* ── Feature grid ── */
159
- .features {
160
- display: grid;
161
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
162
- gap: 16px;
163
- margin-bottom: 48px;
164
- }
165
- .feature {
166
- background: var(--surface);
167
- border: 1px solid var(--border);
168
- border-radius: 14px;
169
- padding: 24px;
170
- transition: border-color .2s, transform .2s;
171
- }
172
- .feature:hover { border-color: var(--primary); transform: translateY(-3px); }
173
- .feature-icon { font-size: 26px; margin-bottom: 12px; }
174
- .feature-title { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
175
- .feature-desc { font-size: 13px; color: var(--muted); line-height: 1.6; }
176
-
177
- /* ── Code block ── */
178
- .code-section { margin-bottom: 48px; }
179
- .code-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--soft); }
180
- .code-block {
181
- background: var(--surface);
182
- border: 1px solid var(--border);
183
- border-radius: 12px;
184
- overflow: hidden;
185
- }
186
- .code-header {
187
- background: var(--surface2);
188
- padding: 10px 16px;
189
- border-bottom: 1px solid var(--border);
190
- display: flex; align-items: center; gap: 8px;
191
- font-size: 12px; color: var(--muted);
192
- }
193
- .code-dots { display: flex; gap: 6px; }
194
- .code-dot { width: 10px; height: 10px; border-radius: 50%; }
195
- .code-dot:nth-child(1){background:#ef4444} .code-dot:nth-child(2){background:#f59e0b} .code-dot:nth-child(3){background:#22c55e}
196
- .code-filename { font-family: 'JetBrains Mono', monospace; margin-left: 4px; }
197
- pre {
198
- padding: 20px 24px;
199
- font-family: 'JetBrains Mono', monospace;
200
- font-size: 13px;
201
- line-height: 1.7;
202
- overflow-x: auto;
203
- color: #94a3b8;
204
- }
205
- .kw { color: #c084fc; } /* keywords */
206
- .fn { color: #818cf8; } /* functions */
207
- .str { color: #34d399; } /* strings */
208
- .cm { color: #475569; font-style: italic; } /* comments */
209
- .cl { color: #f8fafc; } /* class names */
210
- .pm { color: #f59e0b; } /* parameters */
211
-
212
- /* ── Status strip ── */
213
- .status-strip {
214
- display: flex; gap: 12px; flex-wrap: wrap;
215
- margin-bottom: 48px;
216
- }
217
- .status-item {
218
- flex: 1; min-width: 160px;
219
- background: var(--surface);
220
- border: 1px solid var(--border);
221
- border-radius: 12px;
222
- padding: 16px 20px;
223
- display: flex; align-items: center; gap: 12px;
224
- }
225
- .status-icon { font-size: 20px; }
226
- .status-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
227
- .status-value { font-size: 14px; font-weight: 600; color: var(--text); }
228
- .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px rgba(34,197,94,0.5); flex-shrink: 0; }
229
-
230
- /* ── Footer ── */
231
- footer {
232
- text-align: center;
233
- padding-top: 40px;
234
- border-top: 1px solid var(--border);
235
- color: var(--muted);
236
- font-size: 13px;
237
- display: flex; flex-direction: column; gap: 12px; align-items: center;
238
- }
239
- .footer-links { display: flex; gap: 24px; }
240
- .footer-links a { color: var(--muted); text-decoration: none; font-size: 13px; }
241
- .footer-links a:hover { color: var(--primary-h); }
242
-
243
- /* ── Notice ── */
244
- .notice {
245
- background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(168,85,247,0.05));
246
- border: 1px solid rgba(99,102,241,0.2);
247
- border-radius: 12px;
248
- padding: 16px 20px;
249
- font-size: 13px;
250
- color: var(--soft);
251
- margin-bottom: 40px;
252
- display: flex; align-items: flex-start; gap: 12px;
253
- }
254
- .notice-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
255
- code { font-family: 'JetBrains Mono', monospace; background: var(--surface2); color: var(--primary-h); padding: 1px 7px; border-radius: 4px; font-size: 12px; }
67
+ .steps h3{
68
+ margin-bottom:10px;
69
+ font-size:16px;
70
+ }
256
71
 
257
- @media(max-width:600px) {
258
- .page { padding: 40px 16px 60px; }
259
- h1 { font-size: 32px; }
260
- .features { grid-template-columns: 1fr; }
261
- }
262
- </style>
72
+ .steps ul{
73
+ margin:0;
74
+ padding-left:20px;
75
+ color:#555;
76
+ }
77
+
78
+ .links{
79
+ margin-top:35px;
80
+ display:flex;
81
+ justify-content:center;
82
+ gap:18px;
83
+ flex-wrap:wrap;
84
+ }
85
+
86
+ .links a{
87
+ color:#f97316;
88
+ text-decoration:none;
89
+ font-size:14px;
90
+ display:flex;
91
+ align-items:center;
92
+ gap:6px;
93
+ }
94
+
95
+ .links a:hover{
96
+ text-decoration:underline;
97
+ }
98
+
99
+ .footer{
100
+ margin-top:35px;
101
+ font-size:13px;
102
+ color:#888;
103
+ }
104
+
105
+ </style>
263
106
  </head>
107
+
264
108
  <body>
265
- <div class="orb orb-1"></div>
266
- <div class="orb orb-2"></div>
267
-
268
- <div class="page">
269
-
270
- <!-- Hero -->
271
- <div class="hero">
272
- <div class="logo-wrap">⚡</div>
273
- <div class="version-badge">
274
- <span class="version-dot"></span>
275
- Millas v0.1.2 Running
276
- </div>
277
- <h1>The installation<br>worked. <span>Congratulations.</span></h1>
278
- <p class="hero-sub">
279
- You're looking at the default Millas welcome page.
280
- This page disappears the moment you define a route at <code>/</code>.
281
- </p>
282
- <div class="hero-actions">
283
- <a href="/admin" class="btn btn-primary">⚡ Open Admin Panel</a>
284
- <a href="/api/health" class="btn btn-ghost">🔍 API Health Check</a>
285
- </div>
286
- </div>
287
-
288
- <!-- Notice -->
289
- <div class="notice">
290
- <span class="notice-icon">💡</span>
291
- <span>
292
- To remove this page, define your own route in <code>routes/web.js</code>:
293
- <br>
294
- <code style="margin-top:6px;display:inline-block">Route.get('/', (req, res) => res.json({ hello: 'world' }))</code>
295
- </span>
296
- </div>
297
-
298
- <!-- Status strip -->
299
- <div class="status-strip">
300
- <div class="status-item">
301
- <span class="status-dot"></span>
302
- <div>
303
- <div class="status-label">Server</div>
304
- <div class="status-value">Running</div>
305
- </div>
306
- </div>
307
- <div class="status-item">
308
- <span class="status-icon">🗄️</span>
309
- <div>
310
- <div class="status-label">Admin Panel</div>
311
- <div class="status-value"><a href="/admin" style="color:var(--primary-h);text-decoration:none">/admin →</a></div>
312
- </div>
313
- </div>
314
- <div class="status-item">
315
- <span class="status-icon">🌐</span>
316
- <div>
317
- <div class="status-label">API</div>
318
- <div class="status-value"><a href="/api/health" style="color:var(--primary-h);text-decoration:none">/api/health →</a></div>
319
- </div>
320
- </div>
321
- <div class="status-item">
322
- <span class="status-icon">📦</span>
323
- <div>
324
- <div class="status-label">Framework</div>
325
- <div class="status-value">Millas 0.1.2</div>
326
- </div>
327
- </div>
328
- </div>
329
-
330
- <div class="divider"><span>What's included</span></div>
331
-
332
- <!-- Features -->
333
- <div class="features">
334
- <div class="feature">
335
- <div class="feature-icon">🛣️</div>
336
- <div class="feature-title">Expressive Router</div>
337
- <div class="feature-desc">Route groups, prefixes, middleware, resource routes, and the <code>Route.auth()</code> shortcut.</div>
338
- </div>
339
- <div class="feature">
340
- <div class="feature-icon">🗃️</div>
341
- <div class="feature-title">ORM + Migrations</div>
342
- <div class="feature-desc">Model-driven schema with <code>millas makemigrations</code> and <code>millas migrate</code>.</div>
343
- </div>
344
- <div class="feature">
345
- <div class="feature-icon">🔐</div>
346
- <div class="feature-title">Authentication</div>
347
- <div class="feature-desc">JWT out of the box. Register, login, refresh, password reset — all via <code>Auth.login()</code>.</div>
348
- </div>
349
- <div class="feature">
350
- <div class="feature-icon">📬</div>
351
- <div class="feature-title">Mail</div>
352
- <div class="feature-desc">SMTP, SendGrid, Mailgun. Template engine with <code>{{ variable }}</code>, loops, and conditionals.</div>
353
- </div>
354
- <div class="feature">
355
- <div class="feature-icon">⚙️</div>
356
- <div class="feature-title">Queue System</div>
357
- <div class="feature-desc">Background jobs with <code>dispatch(new Job())</code>. Database and sync drivers included.</div>
358
- </div>
359
- <div class="feature">
360
- <div class="feature-icon">📡</div>
361
- <div class="feature-title">Event System</div>
362
- <div class="feature-desc">Fire and listen to events. Listeners can run inline or through the queue.</div>
363
- </div>
364
- <div class="feature">
365
- <div class="feature-icon">⚡</div>
366
- <div class="feature-title">Cache</div>
367
- <div class="feature-desc">Memory, file, and null drivers. Tag-based invalidation with <code>Cache.tags('users').flush()</code>.</div>
368
- </div>
369
- <div class="feature">
370
- <div class="feature-icon">🗂️</div>
371
- <div class="feature-title">File Storage</div>
372
- <div class="feature-desc">Local disk with multiple named disks. Upload, copy, move, list, stream files.</div>
373
- </div>
374
- <div class="feature">
375
- <div class="feature-icon">🛡️</div>
376
- <div class="feature-title">Admin Panel</div>
377
- <div class="feature-desc">Register any model and get a full CRUD dashboard at <a href="/admin" style="color:var(--primary-h)">/admin</a> automatically.</div>
378
- </div>
379
- </div>
380
-
381
- <!-- Quick start code -->
382
- <div class="code-section">
383
- <h2>Your first route</h2>
384
- <div class="code-block">
385
- <div class="code-header">
386
- <div class="code-dots"><div class="code-dot"></div><div class="code-dot"></div><div class="code-dot"></div></div>
387
- <span class="code-filename">routes/web.js</span>
388
- </div>
389
- <pre><span class="cm">// Replace this welcome page by defining GET /</span>
390
- <span class="kw">module</span>.<span class="fn">exports</span> = <span class="kw">function</span> (<span class="pm">Route</span>) {
391
-
392
- <span class="pm">Route</span>.<span class="fn">get</span>(<span class="str">'/'</span>, (<span class="pm">req</span>, <span class="pm">res</span>) => {
393
- <span class="pm">res</span>.<span class="fn">json</span>({ message: <span class="str">'Hello from Millas!'</span> });
394
- });
395
-
396
- <span class="cm">// Register a full resource (index, show, store, update, destroy)</span>
397
- <span class="pm">Route</span>.<span class="fn">resource</span>(<span class="str">'/posts'</span>, <span class="cl">PostController</span>);
398
-
399
- <span class="cm">// Protect routes with auth middleware</span>
400
- <span class="pm">Route</span>.<span class="fn">prefix</span>(<span class="str">'/api'</span>).<span class="fn">middleware</span>([<span class="str">'auth'</span>]).<span class="fn">group</span>(() => {
401
- <span class="pm">Route</span>.<span class="fn">get</span>(<span class="str">'/me'</span>, <span class="cl">UserController</span>, <span class="str">'me'</span>);
402
- });
403
-
404
- <span class="cm">// All auth routes in one line</span>
405
- <span class="pm">Route</span>.<span class="fn">auth</span>(<span class="str">'/auth'</span>);
406
-
407
- };</pre>
408
- </div>
409
- </div>
410
-
411
- <!-- CLI quick reference -->
412
- <div class="code-section">
413
- <h2>Useful CLI commands</h2>
414
- <div class="code-block">
415
- <div class="code-header">
416
- <div class="code-dots"><div class="code-dot"></div><div class="code-dot"></div><div class="code-dot"></div></div>
417
- <span class="code-filename">terminal</span>
418
- </div>
419
- <pre><span class="cm"># Generate files</span>
420
- millas make:controller <span class="cl">PostController</span> --resource
421
- millas make:model <span class="cl">Post</span> --migration
422
- millas make:middleware <span class="cl">AdminOnly</span>
423
- millas make:job <span class="cl">SendEmailJob</span>
424
-
425
- <span class="cm"># Database</span>
426
- millas makemigrations <span class="cm"># detect model changes</span>
427
- millas migrate <span class="cm"># run pending migrations</span>
428
- millas migrate:rollback <span class="cm"># undo last batch</span>
429
-
430
- <span class="cm"># Utilities</span>
431
- millas route:list <span class="cm"># show all registered routes</span>
432
- millas queue:work <span class="cm"># start background job worker</span></pre>
433
- </div>
434
- </div>
435
-
436
- <!-- Footer -->
437
- <footer>
438
- <div class="footer-links">
439
- <a href="/admin">Admin Panel</a>
440
- <a href="/api/health">API Health</a>
441
- <a href="https://www.npmjs.com/package/millas" target="_blank">npm</a>
442
- <a href="https://github.com/millas-framework/millas" target="_blank">GitHub</a>
443
- </div>
444
- <span>Built with Millas · Node.js · Express</span>
445
- </footer>
446
-
447
- </div>
109
+
110
+ <div class="container">
111
+
112
+ <div class="icon">
113
+ <img src="https://www.saaspegasus.com/static/images/web/landing-page/rocket-laptop.39d6be7451e6.svg" alt="Millas" style="width:140px;height:auto;">
114
+ </div>
115
+
116
+ <h1>It worked!</h1>
117
+
118
+ <p class="subtitle">
119
+ Millas is installed and running correctly.
120
+ </p>
121
+
122
+ <div class="steps">
123
+ <h3>Next steps</h3>
124
+ <ul>
125
+ <li>Create your first route in <code>routes/web.js</code></li>
126
+ </ul>
127
+ </div>
128
+
129
+ <div class="links">
130
+
131
+ <a href="/admin">
132
+ <i class="fa-solid fa-gauge"></i>
133
+ Admin panel
134
+ </a>
135
+
136
+ <a href="/api/health">
137
+ <i class="fa-solid fa-heart-pulse"></i>
138
+ Health check
139
+ </a>
140
+
141
+ <a href="https://github.com/millas-framework/millas" target="_blank">
142
+ <i class="fa-brands fa-github"></i>
143
+ GitHub
144
+ </a>
145
+
146
+ </div>
147
+
148
+ <div class="footer">
149
+ Millas v0.1.2
150
+ </div>
151
+
152
+ </div>
153
+
448
154
  </body>
449
155
  </html>`;
450
156
 
@@ -458,22 +164,12 @@ millas queue:work <span class="cm"># start background job worker</span
458
164
  * rejections are forwarded to Express error handlers.
459
165
  */
460
166
  class Router {
461
- /**
462
- * @param {object} expressApp — the Express application
463
- * @param {RouteRegistry} registry
464
- * @param {MiddlewareRegistry} middlewareRegistry
465
- */
466
167
  constructor(expressApp, registry, middlewareRegistry) {
467
168
  this._app = expressApp;
468
169
  this._registry = registry;
469
170
  this._mw = middlewareRegistry || new MiddlewareRegistry();
470
171
  }
471
172
 
472
- /**
473
- * Bind all registered routes onto the Express app.
474
- * Does NOT add 404/error handlers — call mountFallbacks() after
475
- * all other middleware (like Admin) has been registered.
476
- */
477
173
  mountRoutes() {
478
174
  const routes = this._registry.all();
479
175
  for (const route of routes) {
@@ -482,13 +178,7 @@ class Router {
482
178
  return this;
483
179
  }
484
180
 
485
- /**
486
- * Add the 404 + global error handlers.
487
- * Must be called LAST — after all routes and admin panels.
488
- */
489
181
  mountFallbacks() {
490
- // If no route defined for GET /, show a Django-style welcome page.
491
- // Users override this by defining Route.get('/') in routes/web.js
492
182
  const hasRootRoute = this._registry.all().some(
493
183
  r => r.verb === 'GET' && r.path === '/'
494
184
  );
@@ -500,7 +190,6 @@ class Router {
500
190
  });
501
191
  }
502
192
 
503
- // 404 handler
504
193
  this._app.use((req, res) => {
505
194
  res.status(404).json({
506
195
  error: 'Not Found',
@@ -509,7 +198,6 @@ class Router {
509
198
  });
510
199
  });
511
200
 
512
- // Global error handler
513
201
  this._app.use((err, req, res, _next) => {
514
202
  const status = err.status || err.statusCode || 500;
515
203
  const message = err.message || 'Internal Server Error';
@@ -529,35 +217,20 @@ class Router {
529
217
  return this;
530
218
  }
531
219
 
532
- /**
533
- * Bind all registered routes onto the Express app
534
- * AND add 404/error handlers (original behaviour).
535
- */
536
220
  mount() {
537
221
  const routes = this._registry.all();
538
-
539
222
  for (const route of routes) {
540
223
  this._bindRoute(route);
541
224
  }
542
-
543
- // Re-use mountFallbacks for consistency
544
225
  this.mountFallbacks();
545
226
  return this;
546
227
  }
547
228
 
548
- // ─── Private ──────────────────────────────────────────────────────────────
549
-
550
229
  _bindRoute(route) {
551
- const verb = route.verb.toLowerCase();
552
- const path = route.path;
553
-
554
- // Resolve middleware chain
230
+ const verb = route.verb.toLowerCase();
231
+ const path = route.path;
555
232
  const mwHandlers = this._resolveMiddleware(route.middleware || []);
556
-
557
- // Resolve the terminal handler
558
- const terminal = this._resolveHandler(route.handler, route.method);
559
-
560
- // Register on Express: app.get(path, [...mw], handler)
233
+ const terminal = this._resolveHandler(route.handler, route.method);
561
234
  this._app[verb](path, ...mwHandlers, terminal);
562
235
  }
563
236
 
@@ -573,42 +246,28 @@ class Router {
573
246
  }
574
247
 
575
248
  _resolveHandler(handler, method) {
576
- // Case 1: raw async/sync function
577
249
  if (typeof handler === 'function' && !method) {
578
250
  return this._wrapAsync(handler);
579
251
  }
580
-
581
- // Case 2: controller class + method name string
582
252
  if (typeof handler === 'function' && typeof method === 'string') {
583
253
  const instance = new handler();
584
254
  if (typeof instance[method] !== 'function') {
585
- throw new Error(
586
- `Method "${method}" not found on controller "${handler.name}".`
587
- );
255
+ throw new Error(`Method "${method}" not found on controller "${handler.name}".`);
588
256
  }
589
257
  return this._wrapAsync(instance[method].bind(instance));
590
258
  }
591
-
592
- // Case 3: already-instantiated object + method name
593
259
  if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
594
260
  if (typeof handler[method] !== 'function') {
595
261
  throw new Error(`Method "${method}" not found on handler object.`);
596
262
  }
597
263
  return this._wrapAsync(handler[method].bind(handler));
598
264
  }
599
-
600
- // Case 4: plain object/function with no method (fallback)
601
265
  if (typeof handler === 'function') {
602
266
  return this._wrapAsync(handler);
603
267
  }
604
-
605
268
  throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
606
269
  }
607
270
 
608
- /**
609
- * Wrap an async function so rejections are forwarded to next(err).
610
- * Sync functions pass through unchanged.
611
- */
612
271
  _wrapAsync(fn) {
613
272
  if (fn.constructor.name === 'AsyncFunction') {
614
273
  return (req, res, next) => fn(req, res, next).catch(next);