millas 0.1.3 → 0.1.5

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.3",
3
+ "version": "0.1.5",
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,
@@ -2,6 +2,730 @@
2
2
 
3
3
  const MiddlewareRegistry = require('./MiddlewareRegistry');
4
4
 
5
+ // ── Welcome page — shown at / when no route is defined ────────────────────────
6
+ const WELCOME_PAGE = `<!DOCTYPE html>
7
+ <html lang="en">
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
+ }
37
+
38
+ html { scroll-behavior: smooth; }
39
+
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
+ }
47
+
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
+ }
109
+
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
+ }
161
+
162
+ /* ── Main content ── */
163
+ .main {
164
+ padding: 40px 48px 60px;
165
+ max-width: 820px;
166
+ }
167
+
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
+ }
218
+
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
+ }
283
+
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
+ }
339
+
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
+ }
423
+
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>
461
+ </head>
462
+ <body>
463
+
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>
725
+
726
+ </body>
727
+ </html>`;
728
+
5
729
  /**
6
730
  * Router
7
731
  *
@@ -41,6 +765,19 @@ class Router {
41
765
  * Must be called LAST — after all routes and admin panels.
42
766
  */
43
767
  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
+ const hasRootRoute = this._registry.all().some(
771
+ r => r.verb === 'GET' && r.path === '/'
772
+ );
773
+
774
+ if (!hasRootRoute) {
775
+ this._app.get('/', (req, res) => {
776
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
777
+ res.send(WELCOME_PAGE);
778
+ });
779
+ }
780
+
44
781
  // 404 handler
45
782
  this._app.use((req, res) => {
46
783
  res.status(404).json({
@@ -81,34 +818,9 @@ class Router {
81
818
  this._bindRoute(route);
82
819
  }
83
820
 
84
- // Mount 404 handler after all routes
85
- this._app.use((req, res) => {
86
- res.status(404).json({
87
- error: 'Not Found',
88
- message: `Cannot ${req.method} ${req.path}`,
89
- status: 404,
90
- });
91
- });
92
-
93
- // Mount global error handler
94
- this._app.use((err, req, res, _next) => {
95
- const status = err.status || err.statusCode || 500;
96
- const message = err.message || 'Internal Server Error';
97
-
98
- if (status >= 500 && process.env.NODE_ENV !== 'production') {
99
- console.error(err.stack);
100
- }
101
-
102
- res.status(status).json({
103
- error: status >= 500 ? 'Internal Server Error' : message,
104
- message,
105
- status,
106
- // Validation errors (HttpError with errors field)
107
- ...(err.errors && { errors: err.errors }),
108
- // Stack trace in development for 5xx only
109
- ...(status >= 500 && process.env.NODE_ENV !== 'production' && { stack: err.stack }),
110
- });
111
- });
821
+ // Re-use mountFallbacks for consistency
822
+ this.mountFallbacks();
823
+ return this;
112
824
  }
113
825
 
114
826
  // ─── Private ──────────────────────────────────────────────────────────────
@@ -183,4 +895,4 @@ class Router {
183
895
  }
184
896
  }
185
897
 
186
- module.exports = Router;
898
+ module.exports = Router;