millas 0.1.5 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/router/Router.js +135 -754
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "millas",
3
- "version": "0.1.5",
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": {
@@ -6,722 +6,150 @@ 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
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-Avb2QiuDEEvB4bZJYdft2mNjVShBftLdPG8FJ0V7irTLQ8Uo0qcPxh4Plh7eecDqFDBs4pIAczCMWwKY3KDg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
13
- <link rel="preconnect" href="https://fonts.googleapis.com">
14
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
- <link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
16
- <style>
17
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
18
-
19
- :root {
20
- --bg: #f5f3ee;
21
- --bg2: #edeae3;
22
- --white: #ffffff;
23
- --border: #d6d0c4;
24
- --border2: #c4bdb0;
25
- --primary: #2563eb;
26
- --primary-dk: #1d4ed8;
27
- --primary-lt: #eff6ff;
28
- --accent: #059669;
29
- --accent-lt: #ecfdf5;
30
- --warn: #d97706;
31
- --warn-lt: #fffbeb;
32
- --text: #1c1917;
33
- --text2: #44403c;
34
- --muted: #78716c;
35
- --faint: #a8a29e;
36
- }
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
+ }
37
31
 
38
- html { scroll-behavior: smooth; }
32
+ .container{
33
+ max-width:720px;
34
+ padding:40px;
35
+ }
39
36
 
40
- body {
41
- font-family: 'DM Sans', system-ui, sans-serif;
42
- background: var(--bg);
43
- color: var(--text);
44
- min-height: 100vh;
45
- line-height: 1.6;
46
- }
37
+ .icon{
38
+ margin-bottom:20px;
39
+ }
47
40
 
48
- /* ── Top bar ── */
49
- .topbar {
50
- background: var(--white);
51
- border-bottom: 1px solid var(--border);
52
- padding: 0 32px;
53
- display: flex;
54
- align-items: center;
55
- justify-content: space-between;
56
- height: 52px;
57
- position: sticky;
58
- top: 0;
59
- z-index: 100;
60
- }
61
- .topbar-brand {
62
- display: flex;
63
- align-items: center;
64
- gap: 10px;
65
- font-family: 'Libre Baskerville', Georgia, serif;
66
- font-size: 17px;
67
- font-weight: 700;
68
- color: var(--text);
69
- text-decoration: none;
70
- letter-spacing: -0.2px;
71
- }
72
- .topbar-brand .brand-icon {
73
- width: 28px;
74
- height: 28px;
75
- background: var(--primary);
76
- border-radius: 6px;
77
- display: flex;
78
- align-items: center;
79
- justify-content: center;
80
- color: white;
81
- font-size: 13px;
82
- }
83
- .topbar-links {
84
- display: flex;
85
- align-items: center;
86
- gap: 4px;
87
- }
88
- .topbar-links a {
89
- display: flex;
90
- align-items: center;
91
- gap: 6px;
92
- padding: 6px 12px;
93
- border-radius: 6px;
94
- text-decoration: none;
95
- font-size: 13px;
96
- font-weight: 500;
97
- color: var(--muted);
98
- transition: background .15s, color .15s;
99
- }
100
- .topbar-links a:hover { background: var(--bg); color: var(--text); }
101
- .topbar-links a i { font-size: 12px; }
102
-
103
- /* ── Layout ── */
104
- .layout {
105
- display: grid;
106
- grid-template-columns: 220px 1fr;
107
- min-height: calc(100vh - 52px);
108
- }
41
+ h1{
42
+ font-size:32px;
43
+ margin-bottom:12px;
44
+ }
109
45
 
110
- /* ── Sidebar ── */
111
- .sidebar {
112
- background: var(--white);
113
- border-right: 1px solid var(--border);
114
- padding: 28px 0;
115
- position: sticky;
116
- top: 52px;
117
- height: calc(100vh - 52px);
118
- overflow-y: auto;
119
- }
120
- .sidebar-section {
121
- padding: 0 16px 20px;
122
- }
123
- .sidebar-label {
124
- font-size: 10px;
125
- font-weight: 600;
126
- letter-spacing: 0.8px;
127
- text-transform: uppercase;
128
- color: var(--faint);
129
- padding: 0 8px;
130
- margin-bottom: 6px;
131
- }
132
- .sidebar-link {
133
- display: flex;
134
- align-items: center;
135
- gap: 9px;
136
- padding: 7px 10px;
137
- border-radius: 7px;
138
- text-decoration: none;
139
- font-size: 13.5px;
140
- font-weight: 500;
141
- color: var(--text2);
142
- transition: background .15s, color .15s;
143
- margin-bottom: 1px;
144
- }
145
- .sidebar-link i {
146
- width: 16px;
147
- text-align: center;
148
- font-size: 13px;
149
- color: var(--faint);
150
- flex-shrink: 0;
151
- }
152
- .sidebar-link:hover { background: var(--bg); color: var(--text); }
153
- .sidebar-link:hover i { color: var(--primary); }
154
- .sidebar-link.active { background: var(--primary-lt); color: var(--primary); }
155
- .sidebar-link.active i { color: var(--primary); }
156
- .sidebar-divider {
157
- height: 1px;
158
- background: var(--border);
159
- margin: 4px 16px 20px;
160
- }
46
+ .subtitle{
47
+ color:#555;
48
+ font-size:16px;
49
+ line-height:1.7;
50
+ margin-bottom:25px;
51
+ }
161
52
 
162
- /* ── Main content ── */
163
- .main {
164
- padding: 40px 48px 60px;
165
- max-width: 820px;
166
- }
53
+ code{
54
+ background:#fff4ed;
55
+ color:#ea580c;
56
+ padding:4px 8px;
57
+ border-radius:6px;
58
+ font-size:13px;
59
+ }
167
60
 
168
- /* ── Success banner ── */
169
- .success-banner {
170
- background: var(--accent-lt);
171
- border: 1px solid #a7f3d0;
172
- border-left: 4px solid var(--accent);
173
- border-radius: 8px;
174
- padding: 14px 18px;
175
- display: flex;
176
- align-items: flex-start;
177
- gap: 12px;
178
- margin-bottom: 36px;
179
- }
180
- .success-banner i {
181
- color: var(--accent);
182
- font-size: 16px;
183
- margin-top: 1px;
184
- flex-shrink: 0;
185
- }
186
- .success-banner-text { font-size: 14px; color: #065f46; line-height: 1.5; }
187
- .success-banner-text strong { font-weight: 600; display: block; margin-bottom: 2px; }
188
-
189
- /* ── Page heading ── */
190
- .page-heading {
191
- margin-bottom: 36px;
192
- padding-bottom: 28px;
193
- border-bottom: 1px solid var(--border);
194
- }
195
- .page-heading h1 {
196
- font-family: 'Libre Baskerville', Georgia, serif;
197
- font-size: 30px;
198
- font-weight: 700;
199
- letter-spacing: -0.5px;
200
- line-height: 1.2;
201
- color: var(--text);
202
- margin-bottom: 8px;
203
- }
204
- .page-heading p {
205
- font-size: 15px;
206
- color: var(--muted);
207
- max-width: 560px;
208
- }
209
- .page-heading p code {
210
- font-family: 'DM Mono', monospace;
211
- font-size: 12.5px;
212
- background: var(--bg2);
213
- color: var(--primary);
214
- padding: 1px 6px;
215
- border-radius: 4px;
216
- border: 1px solid var(--border);
217
- }
61
+ .steps{
62
+ margin-top:30px;
63
+ text-align:left;
64
+ display:inline-block;
65
+ }
218
66
 
219
- /* ── Info strip ── */
220
- .info-strip {
221
- display: grid;
222
- grid-template-columns: repeat(3, 1fr);
223
- gap: 14px;
224
- margin-bottom: 36px;
225
- }
226
- .info-card {
227
- background: var(--white);
228
- border: 1px solid var(--border);
229
- border-radius: 9px;
230
- padding: 16px 18px;
231
- }
232
- .info-card-label {
233
- font-size: 11px;
234
- font-weight: 600;
235
- letter-spacing: 0.5px;
236
- text-transform: uppercase;
237
- color: var(--faint);
238
- margin-bottom: 6px;
239
- display: flex;
240
- align-items: center;
241
- gap: 6px;
242
- }
243
- .info-card-label i { font-size: 10px; }
244
- .info-card-value {
245
- font-size: 14px;
246
- font-weight: 600;
247
- color: var(--text);
248
- display: flex;
249
- align-items: center;
250
- gap: 7px;
251
- }
252
- .dot-green {
253
- width: 7px; height: 7px; border-radius: 50%;
254
- background: var(--accent);
255
- box-shadow: 0 0 6px rgba(5,150,105,0.4);
256
- flex-shrink: 0;
257
- }
258
- .info-card-value a {
259
- color: var(--primary);
260
- text-decoration: none;
261
- font-weight: 600;
262
- }
263
- .info-card-value a:hover { text-decoration: underline; }
264
-
265
- /* ── Section titles ── */
266
- .section-title {
267
- font-size: 12px;
268
- font-weight: 600;
269
- letter-spacing: 0.6px;
270
- text-transform: uppercase;
271
- color: var(--faint);
272
- margin-bottom: 14px;
273
- display: flex;
274
- align-items: center;
275
- gap: 8px;
276
- }
277
- .section-title::after {
278
- content: '';
279
- flex: 1;
280
- height: 1px;
281
- background: var(--border);
282
- }
67
+ .steps h3{
68
+ margin-bottom:10px;
69
+ font-size:16px;
70
+ }
283
71
 
284
- /* ── Feature table ── */
285
- .feature-table {
286
- background: var(--white);
287
- border: 1px solid var(--border);
288
- border-radius: 10px;
289
- overflow: hidden;
290
- margin-bottom: 36px;
291
- }
292
- .feature-table-row {
293
- display: grid;
294
- grid-template-columns: 40px 1fr;
295
- align-items: start;
296
- padding: 14px 18px;
297
- gap: 14px;
298
- border-bottom: 1px solid var(--border);
299
- transition: background .12s;
300
- }
301
- .feature-table-row:last-child { border-bottom: none; }
302
- .feature-table-row:hover { background: var(--bg); }
303
- .feature-icon-wrap {
304
- width: 34px; height: 34px;
305
- background: var(--bg2);
306
- border: 1px solid var(--border);
307
- border-radius: 8px;
308
- display: flex;
309
- align-items: center;
310
- justify-content: center;
311
- flex-shrink: 0;
312
- margin-top: 1px;
313
- }
314
- .feature-icon-wrap i {
315
- font-size: 14px;
316
- color: var(--primary);
317
- }
318
- .feature-body {}
319
- .feature-name {
320
- font-size: 14px;
321
- font-weight: 600;
322
- color: var(--text);
323
- margin-bottom: 2px;
324
- }
325
- .feature-desc {
326
- font-size: 13px;
327
- color: var(--muted);
328
- line-height: 1.55;
329
- }
330
- .feature-desc code {
331
- font-family: 'DM Mono', monospace;
332
- font-size: 11.5px;
333
- background: var(--bg2);
334
- color: var(--primary);
335
- padding: 1px 5px;
336
- border-radius: 4px;
337
- border: 1px solid var(--border);
338
- }
72
+ .steps ul{
73
+ margin:0;
74
+ padding-left:20px;
75
+ color:#555;
76
+ }
339
77
 
340
- /* ── Code blocks ── */
341
- .code-wrap {
342
- background: var(--white);
343
- border: 1px solid var(--border);
344
- border-radius: 10px;
345
- overflow: hidden;
346
- margin-bottom: 36px;
347
- }
348
- .code-header {
349
- background: var(--bg2);
350
- border-bottom: 1px solid var(--border);
351
- padding: 9px 16px;
352
- display: flex;
353
- align-items: center;
354
- justify-content: space-between;
355
- gap: 10px;
356
- }
357
- .code-header-left {
358
- display: flex;
359
- align-items: center;
360
- gap: 8px;
361
- }
362
- .code-header-left i {
363
- color: var(--faint);
364
- font-size: 12px;
365
- }
366
- .code-filename {
367
- font-family: 'DM Mono', monospace;
368
- font-size: 12px;
369
- color: var(--text2);
370
- font-weight: 500;
371
- }
372
- .code-lang {
373
- font-size: 11px;
374
- font-weight: 600;
375
- letter-spacing: 0.4px;
376
- text-transform: uppercase;
377
- color: var(--faint);
378
- background: var(--border);
379
- padding: 2px 7px;
380
- border-radius: 4px;
381
- }
382
- pre {
383
- padding: 20px 22px;
384
- font-family: 'DM Mono', monospace;
385
- font-size: 12.5px;
386
- line-height: 1.75;
387
- overflow-x: auto;
388
- color: var(--text2);
389
- }
390
- .kw { color: #7c3aed; font-weight: 500; }
391
- .fn { color: #2563eb; }
392
- .str { color: #059669; }
393
- .cm { color: var(--faint); font-style: italic; }
394
- .cl { color: #b45309; }
395
- .pm { color: var(--text); }
396
- .num { color: #dc2626; }
397
-
398
- /* ── Warning notice ── */
399
- .notice {
400
- background: var(--warn-lt);
401
- border: 1px solid #fde68a;
402
- border-left: 4px solid var(--warn);
403
- border-radius: 8px;
404
- padding: 13px 16px;
405
- display: flex;
406
- align-items: flex-start;
407
- gap: 10px;
408
- margin-bottom: 36px;
409
- font-size: 13.5px;
410
- color: #78350f;
411
- line-height: 1.55;
412
- }
413
- .notice i { color: var(--warn); font-size: 14px; margin-top: 2px; flex-shrink: 0; }
414
- .notice code {
415
- font-family: 'DM Mono', monospace;
416
- font-size: 11.5px;
417
- background: #fef3c7;
418
- padding: 1px 5px;
419
- border-radius: 4px;
420
- border: 1px solid #fde68a;
421
- color: #92400e;
422
- }
78
+ .links{
79
+ margin-top:35px;
80
+ display:flex;
81
+ justify-content:center;
82
+ gap:18px;
83
+ flex-wrap:wrap;
84
+ }
423
85
 
424
- /* ── Footer ── */
425
- .page-footer {
426
- padding-top: 28px;
427
- border-top: 1px solid var(--border);
428
- display: flex;
429
- align-items: center;
430
- justify-content: space-between;
431
- flex-wrap: wrap;
432
- gap: 12px;
433
- }
434
- .page-footer-left {
435
- font-size: 13px;
436
- color: var(--faint);
437
- }
438
- .page-footer-links {
439
- display: flex;
440
- gap: 16px;
441
- }
442
- .page-footer-links a {
443
- font-size: 13px;
444
- color: var(--muted);
445
- text-decoration: none;
446
- display: flex;
447
- align-items: center;
448
- gap: 5px;
449
- transition: color .15s;
450
- }
451
- .page-footer-links a i { font-size: 11px; }
452
- .page-footer-links a:hover { color: var(--primary); }
453
-
454
- @media(max-width: 768px) {
455
- .layout { grid-template-columns: 1fr; }
456
- .sidebar { display: none; }
457
- .main { padding: 28px 20px 48px; }
458
- .info-strip { grid-template-columns: 1fr; }
459
- }
460
- </style>
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>
461
106
  </head>
107
+
462
108
  <body>
463
109
 
464
- <!-- Top bar -->
465
- <nav class="topbar">
466
- <a href="/" class="topbar-brand">
467
- <div class="brand-icon"><i class="fa-solid fa-bolt"></i></div>
468
- Millas
469
- </a>
470
- <div class="topbar-links">
471
- <a href="/admin"><i class="fa-solid fa-gauge-high"></i> Admin</a>
472
- <a href="/api/health"><i class="fa-solid fa-heart-pulse"></i> Health</a>
473
- <a href="https://github.com/millas-framework/millas" target="_blank"><i class="fa-brands fa-github"></i> GitHub</a>
474
- </div>
475
- </nav>
476
-
477
- <div class="layout">
478
-
479
- <!-- Sidebar -->
480
- <aside class="sidebar">
481
- <div class="sidebar-section">
482
- <div class="sidebar-label">Navigation</div>
483
- <a href="/admin" class="sidebar-link">
484
- <i class="fa-solid fa-gauge-high"></i> Admin Panel
485
- </a>
486
- <a href="/api/health" class="sidebar-link">
487
- <i class="fa-solid fa-heart-pulse"></i> API Health
488
- </a>
489
- </div>
490
-
491
- <div class="sidebar-divider"></div>
492
-
493
- <div class="sidebar-section">
494
- <div class="sidebar-label">Framework</div>
495
- <a href="#routing" class="sidebar-link active">
496
- <i class="fa-solid fa-route"></i> Routing
497
- </a>
498
- <a href="#database" class="sidebar-link">
499
- <i class="fa-solid fa-database"></i> Database / ORM
500
- </a>
501
- <a href="#auth" class="sidebar-link">
502
- <i class="fa-solid fa-lock"></i> Authentication
503
- </a>
504
- <a href="#mail" class="sidebar-link">
505
- <i class="fa-solid fa-envelope"></i> Mail
506
- </a>
507
- <a href="#queue" class="sidebar-link">
508
- <i class="fa-solid fa-layer-group"></i> Queue
509
- </a>
510
- <a href="#cache" class="sidebar-link">
511
- <i class="fa-solid fa-bolt"></i> Cache
512
- </a>
513
- <a href="#storage" class="sidebar-link">
514
- <i class="fa-solid fa-folder-open"></i> Storage
515
- </a>
516
- </div>
517
-
518
- <div class="sidebar-divider"></div>
519
-
520
- <div class="sidebar-section">
521
- <div class="sidebar-label">External</div>
522
- <a href="https://www.npmjs.com/package/millas" target="_blank" class="sidebar-link">
523
- <i class="fa-brands fa-npm"></i> npm package
524
- </a>
525
- <a href="https://github.com/millas-framework/millas" target="_blank" class="sidebar-link">
526
- <i class="fa-brands fa-github"></i> GitHub
527
- </a>
528
- </div>
529
- </aside>
530
-
531
- <!-- Main -->
532
- <main class="main">
533
-
534
- <!-- Success banner -->
535
- <div class="success-banner">
536
- <i class="fa-solid fa-circle-check"></i>
537
- <div class="success-banner-text">
538
- <strong>Installation successful!</strong>
539
- Millas v0.1.2 is running. Define a route at <code style="font-family:'DM Mono',monospace;font-size:12px;background:rgba(5,150,105,0.08);color:#065f46;padding:1px 6px;border-radius:4px;border:1px solid #a7f3d0">GET /</code> in <code style="font-family:'DM Mono',monospace;font-size:12px;background:rgba(5,150,105,0.08);color:#065f46;padding:1px 6px;border-radius:4px;border:1px solid #a7f3d0">routes/web.js</code> to replace this page.
540
- </div>
541
- </div>
542
-
543
- <!-- Heading -->
544
- <div class="page-heading">
545
- <h1>Millas Framework</h1>
546
- <p>
547
- A Node.js web framework built on Express. This welcome page is shown
548
- because no route is registered at <code>/</code>. Add one to get started.
549
- </p>
550
- </div>
551
-
552
- <!-- Status strip -->
553
- <div class="info-strip">
554
- <div class="info-card">
555
- <div class="info-card-label"><i class="fa-solid fa-server"></i> Server</div>
556
- <div class="info-card-value"><span class="dot-green"></span> Running</div>
557
- </div>
558
- <div class="info-card">
559
- <div class="info-card-label"><i class="fa-solid fa-gauge-high"></i> Admin panel</div>
560
- <div class="info-card-value"><a href="/admin">/admin &rarr;</a></div>
561
- </div>
562
- <div class="info-card">
563
- <div class="info-card-label"><i class="fa-solid fa-heart-pulse"></i> API health</div>
564
- <div class="info-card-value"><a href="/api/health">/api/health &rarr;</a></div>
565
- </div>
566
- </div>
567
-
568
- <!-- Notice -->
569
- <div class="notice">
570
- <i class="fa-solid fa-triangle-exclamation"></i>
571
- <span>
572
- This welcome page is only visible while no <code>GET /</code> route is defined.
573
- As soon as you add one to <code>routes/web.js</code>, it disappears automatically.
574
- </span>
575
- </div>
576
-
577
- <!-- Included features -->
578
- <div class="section-title">What&rsquo;s included</div>
579
- <div class="feature-table">
580
-
581
- <div class="feature-table-row" id="routing">
582
- <div class="feature-icon-wrap"><i class="fa-solid fa-route"></i></div>
583
- <div class="feature-body">
584
- <div class="feature-name">Expressive Router</div>
585
- <div class="feature-desc">Route groups, prefixes, middleware chains, resource routes, and the <code>Route.auth()</code> shortcut for all auth endpoints in one line.</div>
586
- </div>
587
- </div>
588
-
589
- <div class="feature-table-row" id="database">
590
- <div class="feature-icon-wrap"><i class="fa-solid fa-database"></i></div>
591
- <div class="feature-body">
592
- <div class="feature-name">ORM &amp; Migrations</div>
593
- <div class="feature-desc">Model-driven schema management. Run <code>millas makemigrations</code> to detect changes and <code>millas migrate</code> to apply them.</div>
594
- </div>
595
- </div>
596
-
597
- <div class="feature-table-row" id="auth">
598
- <div class="feature-icon-wrap"><i class="fa-solid fa-lock"></i></div>
599
- <div class="feature-body">
600
- <div class="feature-name">Authentication</div>
601
- <div class="feature-desc">JWT out of the box. Register, login, token refresh, and password reset — all accessible via <code>Auth.login()</code>.</div>
602
- </div>
603
- </div>
604
-
605
- <div class="feature-table-row" id="mail">
606
- <div class="feature-icon-wrap"><i class="fa-solid fa-envelope"></i></div>
607
- <div class="feature-body">
608
- <div class="feature-name">Mail</div>
609
- <div class="feature-desc">SMTP, SendGrid, and Mailgun drivers. Template engine with <code>{{ variable }}</code> interpolation, loops, and conditionals.</div>
610
- </div>
611
- </div>
612
-
613
- <div class="feature-table-row" id="queue">
614
- <div class="feature-icon-wrap"><i class="fa-solid fa-layer-group"></i></div>
615
- <div class="feature-body">
616
- <div class="feature-name">Queue System</div>
617
- <div class="feature-desc">Background job processing with <code>dispatch(new Job())</code>. Database and synchronous drivers included.</div>
618
- </div>
619
- </div>
620
-
621
- <div class="feature-table-row">
622
- <div class="feature-icon-wrap"><i class="fa-solid fa-satellite-dish"></i></div>
623
- <div class="feature-body">
624
- <div class="feature-name">Event System</div>
625
- <div class="feature-desc">Fire and listen to events across your application. Listeners can run inline or be pushed through the queue.</div>
626
- </div>
627
- </div>
628
-
629
- <div class="feature-table-row" id="cache">
630
- <div class="feature-icon-wrap"><i class="fa-solid fa-bolt"></i></div>
631
- <div class="feature-body">
632
- <div class="feature-name">Cache</div>
633
- <div class="feature-desc">Memory, file, and null drivers. Tag-based invalidation with <code>Cache.tags('users').flush()</code>.</div>
634
- </div>
635
- </div>
636
-
637
- <div class="feature-table-row" id="storage">
638
- <div class="feature-icon-wrap"><i class="fa-solid fa-folder-open"></i></div>
639
- <div class="feature-body">
640
- <div class="feature-name">File Storage</div>
641
- <div class="feature-desc">Local disk with multiple named disks. Upload, copy, move, list, and stream files.</div>
642
- </div>
643
- </div>
644
-
645
- <div class="feature-table-row">
646
- <div class="feature-icon-wrap"><i class="fa-solid fa-shield-halved"></i></div>
647
- <div class="feature-body">
648
- <div class="feature-name">Admin Panel</div>
649
- <div class="feature-desc">Register any model and get a full CRUD dashboard at <a href="/admin" style="color:var(--primary)">/admin</a> automatically — no extra configuration required.</div>
650
- </div>
651
- </div>
652
-
653
- </div>
654
-
655
- <!-- Quick start code -->
656
- <div class="section-title">Quick start</div>
657
- <div class="code-wrap">
658
- <div class="code-header">
659
- <div class="code-header-left">
660
- <i class="fa-regular fa-file-code"></i>
661
- <span class="code-filename">routes/web.js</span>
662
- </div>
663
- <span class="code-lang">JavaScript</span>
664
- </div>
665
- <pre><span class="cm">// Replace this welcome page by defining GET /</span>
666
- <span class="kw">module</span>.<span class="fn">exports</span> = <span class="kw">function</span> (<span class="pm">Route</span>) {
667
-
668
- <span class="pm">Route</span>.<span class="fn">get</span>(<span class="str">'/'</span>, (<span class="pm">req</span>, <span class="pm">res</span>) => {
669
- <span class="pm">res</span>.<span class="fn">json</span>({ message: <span class="str">'Hello from Millas!'</span> });
670
- });
671
-
672
- <span class="cm">// Register a full resource (index, show, store, update, destroy)</span>
673
- <span class="pm">Route</span>.<span class="fn">resource</span>(<span class="str">'/posts'</span>, <span class="cl">PostController</span>);
674
-
675
- <span class="cm">// Protect routes with auth middleware</span>
676
- <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>(() => {
677
- <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>);
678
- });
679
-
680
- <span class="cm">// All auth routes in one line</span>
681
- <span class="pm">Route</span>.<span class="fn">auth</span>(<span class="str">'/auth'</span>);
682
-
683
- };</pre>
684
- </div>
685
-
686
- <!-- CLI reference -->
687
- <div class="section-title">CLI reference</div>
688
- <div class="code-wrap">
689
- <div class="code-header">
690
- <div class="code-header-left">
691
- <i class="fa-solid fa-terminal"></i>
692
- <span class="code-filename">terminal</span>
693
- </div>
694
- <span class="code-lang">Shell</span>
695
- </div>
696
- <pre><span class="cm"># Generate files</span>
697
- millas make:controller <span class="cl">PostController</span> --resource
698
- millas make:model <span class="cl">Post</span> --migration
699
- millas make:middleware <span class="cl">AdminOnly</span>
700
- millas make:job <span class="cl">SendEmailJob</span>
701
-
702
- <span class="cm"># Database</span>
703
- millas makemigrations <span class="cm"># detect model changes</span>
704
- millas migrate <span class="cm"># run pending migrations</span>
705
- millas migrate:rollback <span class="cm"># undo last batch</span>
706
-
707
- <span class="cm"># Utilities</span>
708
- millas route:list <span class="cm"># show all registered routes</span>
709
- millas queue:work <span class="cm"># start background job worker</span></pre>
710
- </div>
711
-
712
- <!-- Footer -->
713
- <div class="page-footer">
714
- <div class="page-footer-left">Millas v0.1.2 &mdash; Built on Node.js &amp; Express</div>
715
- <div class="page-footer-links">
716
- <a href="/admin"><i class="fa-solid fa-gauge-high"></i> Admin</a>
717
- <a href="/api/health"><i class="fa-solid fa-heart-pulse"></i> Health</a>
718
- <a href="https://www.npmjs.com/package/millas" target="_blank"><i class="fa-brands fa-npm"></i> npm</a>
719
- <a href="https://github.com/millas-framework/millas" target="_blank"><i class="fa-brands fa-github"></i> GitHub</a>
720
- </div>
721
- </div>
722
-
723
- </main>
724
- </div>
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>
725
153
 
726
154
  </body>
727
155
  </html>`;
@@ -736,22 +164,12 @@ millas queue:work <span class="cm"># start background job worker</span
736
164
  * rejections are forwarded to Express error handlers.
737
165
  */
738
166
  class Router {
739
- /**
740
- * @param {object} expressApp — the Express application
741
- * @param {RouteRegistry} registry
742
- * @param {MiddlewareRegistry} middlewareRegistry
743
- */
744
167
  constructor(expressApp, registry, middlewareRegistry) {
745
168
  this._app = expressApp;
746
169
  this._registry = registry;
747
170
  this._mw = middlewareRegistry || new MiddlewareRegistry();
748
171
  }
749
172
 
750
- /**
751
- * Bind all registered routes onto the Express app.
752
- * Does NOT add 404/error handlers — call mountFallbacks() after
753
- * all other middleware (like Admin) has been registered.
754
- */
755
173
  mountRoutes() {
756
174
  const routes = this._registry.all();
757
175
  for (const route of routes) {
@@ -760,13 +178,7 @@ class Router {
760
178
  return this;
761
179
  }
762
180
 
763
- /**
764
- * Add the 404 + global error handlers.
765
- * Must be called LAST — after all routes and admin panels.
766
- */
767
181
  mountFallbacks() {
768
- // If no route defined for GET /, show a Django-style welcome page.
769
- // Users override this by defining Route.get('/') in routes/web.js
770
182
  const hasRootRoute = this._registry.all().some(
771
183
  r => r.verb === 'GET' && r.path === '/'
772
184
  );
@@ -778,7 +190,6 @@ class Router {
778
190
  });
779
191
  }
780
192
 
781
- // 404 handler
782
193
  this._app.use((req, res) => {
783
194
  res.status(404).json({
784
195
  error: 'Not Found',
@@ -787,7 +198,6 @@ class Router {
787
198
  });
788
199
  });
789
200
 
790
- // Global error handler
791
201
  this._app.use((err, req, res, _next) => {
792
202
  const status = err.status || err.statusCode || 500;
793
203
  const message = err.message || 'Internal Server Error';
@@ -807,35 +217,20 @@ class Router {
807
217
  return this;
808
218
  }
809
219
 
810
- /**
811
- * Bind all registered routes onto the Express app
812
- * AND add 404/error handlers (original behaviour).
813
- */
814
220
  mount() {
815
221
  const routes = this._registry.all();
816
-
817
222
  for (const route of routes) {
818
223
  this._bindRoute(route);
819
224
  }
820
-
821
- // Re-use mountFallbacks for consistency
822
225
  this.mountFallbacks();
823
226
  return this;
824
227
  }
825
228
 
826
- // ─── Private ──────────────────────────────────────────────────────────────
827
-
828
229
  _bindRoute(route) {
829
- const verb = route.verb.toLowerCase();
830
- const path = route.path;
831
-
832
- // Resolve middleware chain
230
+ const verb = route.verb.toLowerCase();
231
+ const path = route.path;
833
232
  const mwHandlers = this._resolveMiddleware(route.middleware || []);
834
-
835
- // Resolve the terminal handler
836
- const terminal = this._resolveHandler(route.handler, route.method);
837
-
838
- // Register on Express: app.get(path, [...mw], handler)
233
+ const terminal = this._resolveHandler(route.handler, route.method);
839
234
  this._app[verb](path, ...mwHandlers, terminal);
840
235
  }
841
236
 
@@ -851,42 +246,28 @@ class Router {
851
246
  }
852
247
 
853
248
  _resolveHandler(handler, method) {
854
- // Case 1: raw async/sync function
855
249
  if (typeof handler === 'function' && !method) {
856
250
  return this._wrapAsync(handler);
857
251
  }
858
-
859
- // Case 2: controller class + method name string
860
252
  if (typeof handler === 'function' && typeof method === 'string') {
861
253
  const instance = new handler();
862
254
  if (typeof instance[method] !== 'function') {
863
- throw new Error(
864
- `Method "${method}" not found on controller "${handler.name}".`
865
- );
255
+ throw new Error(`Method "${method}" not found on controller "${handler.name}".`);
866
256
  }
867
257
  return this._wrapAsync(instance[method].bind(instance));
868
258
  }
869
-
870
- // Case 3: already-instantiated object + method name
871
259
  if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
872
260
  if (typeof handler[method] !== 'function') {
873
261
  throw new Error(`Method "${method}" not found on handler object.`);
874
262
  }
875
263
  return this._wrapAsync(handler[method].bind(handler));
876
264
  }
877
-
878
- // Case 4: plain object/function with no method (fallback)
879
265
  if (typeof handler === 'function') {
880
266
  return this._wrapAsync(handler);
881
267
  }
882
-
883
268
  throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
884
269
  }
885
270
 
886
- /**
887
- * Wrap an async function so rejections are forwarded to next(err).
888
- * Sync functions pass through unchanged.
889
- */
890
271
  _wrapAsync(fn) {
891
272
  if (fn.constructor.name === 'AsyncFunction') {
892
273
  return (req, res, next) => fn(req, res, next).catch(next);