millas 0.1.2 → 0.1.4
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 +3 -2
- package/src/admin/Admin.js +311 -503
- package/src/admin/views/layouts/base.njk +468 -0
- package/src/admin/views/pages/dashboard.njk +84 -0
- package/src/admin/views/pages/form.njk +145 -0
- package/src/admin/views/pages/list.njk +164 -0
- package/src/container/Application.js +32 -1
- package/src/router/Router.js +490 -8
- package/src/scaffold/templates.js +22 -29
package/src/router/Router.js
CHANGED
|
@@ -2,6 +2,452 @@
|
|
|
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
|
+
<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
|
+
}
|
|
32
|
+
|
|
33
|
+
html { scroll-behavior: smooth; }
|
|
34
|
+
|
|
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
|
+
}
|
|
43
|
+
|
|
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
|
+
}
|
|
56
|
+
|
|
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
|
+
}
|
|
114
|
+
|
|
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
|
+
}
|
|
122
|
+
|
|
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); }
|
|
144
|
+
|
|
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; }
|
|
256
|
+
|
|
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>
|
|
263
|
+
</head>
|
|
264
|
+
<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>
|
|
448
|
+
</body>
|
|
449
|
+
</html>`;
|
|
450
|
+
|
|
5
451
|
/**
|
|
6
452
|
* Router
|
|
7
453
|
*
|
|
@@ -25,15 +471,36 @@ class Router {
|
|
|
25
471
|
|
|
26
472
|
/**
|
|
27
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.
|
|
28
476
|
*/
|
|
29
|
-
|
|
477
|
+
mountRoutes() {
|
|
30
478
|
const routes = this._registry.all();
|
|
31
|
-
|
|
32
479
|
for (const route of routes) {
|
|
33
480
|
this._bindRoute(route);
|
|
34
481
|
}
|
|
482
|
+
return this;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Add the 404 + global error handlers.
|
|
487
|
+
* Must be called LAST — after all routes and admin panels.
|
|
488
|
+
*/
|
|
489
|
+
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
|
+
const hasRootRoute = this._registry.all().some(
|
|
493
|
+
r => r.verb === 'GET' && r.path === '/'
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (!hasRootRoute) {
|
|
497
|
+
this._app.get('/', (req, res) => {
|
|
498
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
499
|
+
res.send(WELCOME_PAGE);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
35
502
|
|
|
36
|
-
//
|
|
503
|
+
// 404 handler
|
|
37
504
|
this._app.use((req, res) => {
|
|
38
505
|
res.status(404).json({
|
|
39
506
|
error: 'Not Found',
|
|
@@ -42,7 +509,7 @@ class Router {
|
|
|
42
509
|
});
|
|
43
510
|
});
|
|
44
511
|
|
|
45
|
-
//
|
|
512
|
+
// Global error handler
|
|
46
513
|
this._app.use((err, req, res, _next) => {
|
|
47
514
|
const status = err.status || err.statusCode || 500;
|
|
48
515
|
const message = err.message || 'Internal Server Error';
|
|
@@ -55,12 +522,27 @@ class Router {
|
|
|
55
522
|
error: status >= 500 ? 'Internal Server Error' : message,
|
|
56
523
|
message,
|
|
57
524
|
status,
|
|
58
|
-
|
|
59
|
-
...(err.errors && { errors: err.errors }),
|
|
60
|
-
// Stack trace in development for 5xx only
|
|
525
|
+
...(err.errors && { errors: err.errors }),
|
|
61
526
|
...(status >= 500 && process.env.NODE_ENV !== 'production' && { stack: err.stack }),
|
|
62
527
|
});
|
|
63
528
|
});
|
|
529
|
+
return this;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Bind all registered routes onto the Express app
|
|
534
|
+
* AND add 404/error handlers (original behaviour).
|
|
535
|
+
*/
|
|
536
|
+
mount() {
|
|
537
|
+
const routes = this._registry.all();
|
|
538
|
+
|
|
539
|
+
for (const route of routes) {
|
|
540
|
+
this._bindRoute(route);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Re-use mountFallbacks for consistency
|
|
544
|
+
this.mountFallbacks();
|
|
545
|
+
return this;
|
|
64
546
|
}
|
|
65
547
|
|
|
66
548
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
@@ -135,4 +617,4 @@ class Router {
|
|
|
135
617
|
}
|
|
136
618
|
}
|
|
137
619
|
|
|
138
|
-
module.exports = Router;
|
|
620
|
+
module.exports = Router;
|
|
@@ -90,35 +90,25 @@ require('dotenv').config();
|
|
|
90
90
|
|
|
91
91
|
const express = require('express');
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
* Resolve the millas package whether installed locally (node_modules)
|
|
95
|
-
* or used from a globally installed CLI (millas serve).
|
|
96
|
-
*/
|
|
97
93
|
function resolveMillas() {
|
|
98
|
-
// 1. Try local node_modules first (preferred)
|
|
99
94
|
try { return require('millas/src'); } catch {}
|
|
100
|
-
// 2. Try resolving from the global CLI location
|
|
101
95
|
try {
|
|
102
96
|
const path = require('path');
|
|
103
97
|
const cliPath = require.resolve('millas/bin/millas.js');
|
|
104
98
|
const millasSrc = path.join(path.dirname(cliPath), '..', 'src', 'index.js');
|
|
105
99
|
return require(millasSrc);
|
|
106
100
|
} catch {}
|
|
107
|
-
throw new Error(
|
|
108
|
-
'Cannot find millas. Run: npm install millas\\n' +
|
|
109
|
-
'Or install globally: npm install -g millas'
|
|
110
|
-
);
|
|
101
|
+
throw new Error('Cannot find millas. Run: npm install millas');
|
|
111
102
|
}
|
|
112
103
|
|
|
113
104
|
const {
|
|
114
105
|
Application,
|
|
115
|
-
|
|
106
|
+
Admin,
|
|
116
107
|
CacheServiceProvider,
|
|
117
108
|
StorageServiceProvider,
|
|
118
109
|
MailServiceProvider,
|
|
119
110
|
QueueServiceProvider,
|
|
120
111
|
EventServiceProvider,
|
|
121
|
-
AuthServiceProvider,
|
|
122
112
|
} = resolveMillas();
|
|
123
113
|
|
|
124
114
|
const AppServiceProvider = require('../providers/AppServiceProvider');
|
|
@@ -138,7 +128,6 @@ app.providers([
|
|
|
138
128
|
QueueServiceProvider,
|
|
139
129
|
EventServiceProvider,
|
|
140
130
|
AppServiceProvider,
|
|
141
|
-
// AuthServiceProvider, // uncomment when you have a User model
|
|
142
131
|
]);
|
|
143
132
|
|
|
144
133
|
// ── Define routes ────────────────────────────────────────────────
|
|
@@ -152,7 +141,17 @@ app.routes(Route => {
|
|
|
152
141
|
await app.boot();
|
|
153
142
|
|
|
154
143
|
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
155
|
-
app
|
|
144
|
+
// Register app routes first (without 404 handler yet)
|
|
145
|
+
app.mountRoutes();
|
|
146
|
+
|
|
147
|
+
// Admin panel mounts here — before the 404 fallback
|
|
148
|
+
// To disable: comment out this line
|
|
149
|
+
// To change path: Admin.configure({ prefix: '/cms' });
|
|
150
|
+
Admin.mount(expressApp);
|
|
151
|
+
|
|
152
|
+
// Add 404 + error handlers LAST
|
|
153
|
+
app.mountFallbacks();
|
|
154
|
+
|
|
156
155
|
app.listen();
|
|
157
156
|
}
|
|
158
157
|
})();
|
|
@@ -169,19 +168,13 @@ module.exports = { app, expressApp, get route() { return app.route; } };
|
|
|
169
168
|
* Define your web-facing routes here using the Millas Route API.
|
|
170
169
|
*
|
|
171
170
|
* Route.get('/path', ControllerClass, 'method')
|
|
172
|
-
* Route.get('/path', (req, res) => res.json({ ... }))
|
|
173
|
-
* Route.resource('/posts', PostController)
|
|
174
|
-
* Route.group({ prefix: '/
|
|
171
|
+
* Route.get('/path', (req, res) => res.json({ ... }))
|
|
172
|
+
* Route.resource('/posts', PostController)
|
|
173
|
+
* Route.group({ prefix: '/v1', middleware: ['auth'] }, () => { ... })
|
|
174
|
+
* Route.auth('/auth') — registers all auth routes
|
|
175
175
|
*/
|
|
176
176
|
module.exports = function (Route) {
|
|
177
|
-
|
|
178
|
-
res.json({
|
|
179
|
-
framework: 'Millas',
|
|
180
|
-
version: '0.1.0',
|
|
181
|
-
message: 'Welcome to your Millas application!',
|
|
182
|
-
docs: 'https://millas.dev/docs',
|
|
183
|
-
});
|
|
184
|
-
});
|
|
177
|
+
// Your web routes here
|
|
185
178
|
};
|
|
186
179
|
`,
|
|
187
180
|
|
|
@@ -189,10 +182,7 @@ module.exports = function (Route) {
|
|
|
189
182
|
'routes/api.js': `'use strict';
|
|
190
183
|
|
|
191
184
|
/**
|
|
192
|
-
* API Routes
|
|
193
|
-
*
|
|
194
|
-
* All routes here are prefixed with /api.
|
|
195
|
-
* Add Route.middleware(['auth']) to protect routes.
|
|
185
|
+
* API Routes — all routes are prefixed with /api
|
|
196
186
|
*/
|
|
197
187
|
module.exports = function (Route) {
|
|
198
188
|
Route.prefix('/api').group(() => {
|
|
@@ -201,6 +191,9 @@ module.exports = function (Route) {
|
|
|
201
191
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
202
192
|
});
|
|
203
193
|
|
|
194
|
+
// Your API routes here
|
|
195
|
+
// Route.resource('/users', UserController);
|
|
196
|
+
|
|
204
197
|
});
|
|
205
198
|
};
|
|
206
199
|
`,
|