millas 0.1.2 → 0.1.3
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 +48 -0
- package/src/scaffold/templates.js +22 -29
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{% extends "layouts/base.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}{% endblock %}
|
|
4
|
+
{% block topbar_title %}{{ resource.icon }} {{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="breadcrumb">
|
|
8
|
+
<a href="{{ adminPrefix }}/">Dashboard</a>
|
|
9
|
+
<span class="breadcrumb-sep">›</span>
|
|
10
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}">{{ resource.label }}</a>
|
|
11
|
+
<span class="breadcrumb-sep">›</span>
|
|
12
|
+
<span>{{ 'Edit #' + record.id if isEdit else 'New' }}</span>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="card" style="max-width:720px">
|
|
16
|
+
<div class="card-header">
|
|
17
|
+
<span class="card-title">{{ 'Edit ' + resource.singular + ' #' + record.id if isEdit else 'Create ' + resource.singular }}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="card-body">
|
|
20
|
+
<form method="POST" action="{{ formAction }}">
|
|
21
|
+
{% if isEdit %}
|
|
22
|
+
<input type="hidden" name="_method" value="PUT">
|
|
23
|
+
{% endif %}
|
|
24
|
+
|
|
25
|
+
{% if errors | length %}
|
|
26
|
+
<div class="alert alert-error" style="margin-bottom:20px">
|
|
27
|
+
<div>
|
|
28
|
+
<strong>Please fix the following errors:</strong>
|
|
29
|
+
<ul style="margin-top:6px;padding-left:16px">
|
|
30
|
+
{% for field, msgs in errors %}
|
|
31
|
+
<li>{{ msgs[0] }}</li>
|
|
32
|
+
{% endfor %}
|
|
33
|
+
</ul>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
{% endif %}
|
|
37
|
+
|
|
38
|
+
<div class="form-grid">
|
|
39
|
+
{% for field in formFields %}
|
|
40
|
+
<div class="form-group {% if field.type == 'textarea' or field.type == 'json' %}full{% endif %}">
|
|
41
|
+
<label class="form-label">
|
|
42
|
+
{{ field.label }}
|
|
43
|
+
{% if field.required %}<span class="required">*</span>{% endif %}
|
|
44
|
+
</label>
|
|
45
|
+
|
|
46
|
+
{% set val = record[field.name] if record[field.name] is defined else '' %}
|
|
47
|
+
{% set hasError = errors[field.name] is defined %}
|
|
48
|
+
|
|
49
|
+
{% if field.type == 'select' and field.options %}
|
|
50
|
+
<select name="{{ field.name }}" class="form-control {% if hasError %}error{% endif %}">
|
|
51
|
+
<option value="">— Select —</option>
|
|
52
|
+
{% for opt in field.options %}
|
|
53
|
+
<option value="{{ opt }}" {% if val == opt %}selected{% endif %}>{{ opt }}</option>
|
|
54
|
+
{% endfor %}
|
|
55
|
+
</select>
|
|
56
|
+
|
|
57
|
+
{% elif field.type == 'boolean' %}
|
|
58
|
+
<select name="{{ field.name }}" class="form-control">
|
|
59
|
+
<option value="1" {% if val %}selected{% endif %}>Yes</option>
|
|
60
|
+
<option value="0" {% if not val %}selected{% endif %}>No</option>
|
|
61
|
+
</select>
|
|
62
|
+
|
|
63
|
+
{% elif field.type == 'textarea' %}
|
|
64
|
+
<textarea
|
|
65
|
+
name="{{ field.name }}"
|
|
66
|
+
class="form-control {% if hasError %}error{% endif %}"
|
|
67
|
+
placeholder="{{ field.placeholder or '' }}"
|
|
68
|
+
rows="4">{{ val }}</textarea>
|
|
69
|
+
|
|
70
|
+
{% elif field.type == 'json' %}
|
|
71
|
+
<textarea
|
|
72
|
+
name="{{ field.name }}"
|
|
73
|
+
class="form-control {% if hasError %}error{% endif %}"
|
|
74
|
+
placeholder='{"key": "value"}'
|
|
75
|
+
style="font-family:monospace;font-size:12px"
|
|
76
|
+
rows="5">{{ val | dump if val else '' }}</textarea>
|
|
77
|
+
|
|
78
|
+
{% elif field.type == 'password' %}
|
|
79
|
+
<input
|
|
80
|
+
type="password"
|
|
81
|
+
name="{{ field.name }}"
|
|
82
|
+
class="form-control {% if hasError %}error{% endif %}"
|
|
83
|
+
placeholder="{{ 'Leave blank to keep current' if isEdit else '' }}">
|
|
84
|
+
|
|
85
|
+
{% elif field.type == 'email' %}
|
|
86
|
+
<input
|
|
87
|
+
type="email"
|
|
88
|
+
name="{{ field.name }}"
|
|
89
|
+
value="{{ val }}"
|
|
90
|
+
class="form-control {% if hasError %}error{% endif %}"
|
|
91
|
+
placeholder="{{ field.placeholder or '' }}">
|
|
92
|
+
|
|
93
|
+
{% elif field.type == 'number' %}
|
|
94
|
+
<input
|
|
95
|
+
type="number"
|
|
96
|
+
name="{{ field.name }}"
|
|
97
|
+
value="{{ val }}"
|
|
98
|
+
class="form-control {% if hasError %}error{% endif %}"
|
|
99
|
+
placeholder="{{ field.placeholder or '0' }}">
|
|
100
|
+
|
|
101
|
+
{% elif field.type == 'date' %}
|
|
102
|
+
<input
|
|
103
|
+
type="date"
|
|
104
|
+
name="{{ field.name }}"
|
|
105
|
+
value="{{ val }}"
|
|
106
|
+
class="form-control {% if hasError %}error{% endif %}">
|
|
107
|
+
|
|
108
|
+
{% else %}
|
|
109
|
+
<input
|
|
110
|
+
type="text"
|
|
111
|
+
name="{{ field.name }}"
|
|
112
|
+
value="{{ val }}"
|
|
113
|
+
class="form-control {% if hasError %}error{% endif %}"
|
|
114
|
+
placeholder="{{ field.placeholder or '' }}">
|
|
115
|
+
{% endif %}
|
|
116
|
+
|
|
117
|
+
{% if hasError %}
|
|
118
|
+
<span class="form-error">{{ errors[field.name][0] }}</span>
|
|
119
|
+
{% elif field.help %}
|
|
120
|
+
<span class="form-help">{{ field.help }}</span>
|
|
121
|
+
{% endif %}
|
|
122
|
+
</div>
|
|
123
|
+
{% endfor %}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="flex items-center gap-2" style="margin-top:24px;padding-top:20px;border-top:1px solid var(--border)">
|
|
127
|
+
<button type="submit" class="btn btn-primary">
|
|
128
|
+
{{ '💾 Save Changes' if isEdit else '+ Create ' + resource.singular }}
|
|
129
|
+
</button>
|
|
130
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">Cancel</a>
|
|
131
|
+
{% if isEdit and resource.canDelete %}
|
|
132
|
+
<button type="button" class="btn btn-danger" style="margin-left:auto"
|
|
133
|
+
onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/delete', '{{ resource.singular }} #{{ record.id }}')">
|
|
134
|
+
Delete
|
|
135
|
+
</button>
|
|
136
|
+
{% endif %}
|
|
137
|
+
</div>
|
|
138
|
+
</form>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<style>
|
|
143
|
+
.form-control.error { border-color: var(--danger); }
|
|
144
|
+
</style>
|
|
145
|
+
{% endblock %}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
{% extends "layouts/base.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ resource.label }}{% endblock %}
|
|
4
|
+
{% block topbar_title %}{{ resource.icon }} {{ resource.label }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block topbar_actions %}
|
|
7
|
+
{% if resource.canCreate %}
|
|
8
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/create" class="btn btn-primary">
|
|
9
|
+
<span>+</span> New {{ resource.singular }}
|
|
10
|
+
</a>
|
|
11
|
+
{% endif %}
|
|
12
|
+
{% endblock %}
|
|
13
|
+
|
|
14
|
+
{% block content %}
|
|
15
|
+
<div class="breadcrumb">
|
|
16
|
+
<a href="{{ adminPrefix }}/">Dashboard</a>
|
|
17
|
+
<span class="breadcrumb-sep">›</span>
|
|
18
|
+
<span>{{ resource.label }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="card">
|
|
22
|
+
{# ── Toolbar ── #}
|
|
23
|
+
<div class="card-header">
|
|
24
|
+
<span class="card-title text-muted text-sm">{{ total }} record{{ 's' if total != 1 }}</span>
|
|
25
|
+
<div class="flex items-center gap-2 ml-auto">
|
|
26
|
+
{# Search #}
|
|
27
|
+
<form method="GET" action="{{ adminPrefix }}/{{ resource.slug }}" style="display:flex;gap:8px">
|
|
28
|
+
<input type="hidden" name="sort" value="{{ sort }}">
|
|
29
|
+
<input type="hidden" name="order" value="{{ order }}">
|
|
30
|
+
{% if filters | length %}
|
|
31
|
+
{% for key, val in activeFilters %}
|
|
32
|
+
<input type="hidden" name="filter[{{ key }}]" value="{{ val }}">
|
|
33
|
+
{% endfor %}
|
|
34
|
+
{% endif %}
|
|
35
|
+
<div class="search-wrap">
|
|
36
|
+
<span class="search-icon">🔍</span>
|
|
37
|
+
<input
|
|
38
|
+
type="text" name="search"
|
|
39
|
+
value="{{ search }}"
|
|
40
|
+
placeholder="Search..."
|
|
41
|
+
class="form-control search-input">
|
|
42
|
+
</div>
|
|
43
|
+
<button type="submit" class="btn btn-ghost">Search</button>
|
|
44
|
+
{% if search %}
|
|
45
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">✕ Clear</a>
|
|
46
|
+
{% endif %}
|
|
47
|
+
</form>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{# ── Filters row ── #}
|
|
52
|
+
{% if filters | length %}
|
|
53
|
+
<div style="padding:12px 20px;border-bottom:1px solid var(--border);display:flex;gap:10px;flex-wrap:wrap">
|
|
54
|
+
<form method="GET" action="{{ adminPrefix }}/{{ resource.slug }}" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
55
|
+
<input type="hidden" name="search" value="{{ search }}">
|
|
56
|
+
<input type="hidden" name="sort" value="{{ sort }}">
|
|
57
|
+
<input type="hidden" name="order" value="{{ order }}">
|
|
58
|
+
{% for filter in filters %}
|
|
59
|
+
<div style="display:flex;flex-direction:column;gap:4px">
|
|
60
|
+
<label class="form-label" style="font-size:10px">{{ filter.label }}</label>
|
|
61
|
+
{% if filter.type == 'select' %}
|
|
62
|
+
<select name="filter[{{ filter.name }}]" class="form-control" style="width:auto;min-width:120px" onchange="this.form.submit()">
|
|
63
|
+
<option value="">All</option>
|
|
64
|
+
{% for opt in filter.options %}
|
|
65
|
+
<option value="{{ opt }}" {% if activeFilters[filter.name] == opt %}selected{% endif %}>{{ opt }}</option>
|
|
66
|
+
{% endfor %}
|
|
67
|
+
</select>
|
|
68
|
+
{% elif filter.type == 'boolean' %}
|
|
69
|
+
<select name="filter[{{ filter.name }}]" class="form-control" style="width:auto;min-width:100px" onchange="this.form.submit()">
|
|
70
|
+
<option value="">All</option>
|
|
71
|
+
<option value="1" {% if activeFilters[filter.name] == '1' %}selected{% endif %}>Yes</option>
|
|
72
|
+
<option value="0" {% if activeFilters[filter.name] == '0' %}selected{% endif %}>No</option>
|
|
73
|
+
</select>
|
|
74
|
+
{% endif %}
|
|
75
|
+
</div>
|
|
76
|
+
{% endfor %}
|
|
77
|
+
{% if activeFilters | length %}
|
|
78
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}{% if search %}?search={{ search }}{% endif %}" class="btn btn-ghost btn-sm" style="align-self:flex-end">✕ Reset filters</a>
|
|
79
|
+
{% endif %}
|
|
80
|
+
</form>
|
|
81
|
+
</div>
|
|
82
|
+
{% endif %}
|
|
83
|
+
|
|
84
|
+
{# ── Table ── #}
|
|
85
|
+
<div class="table-wrap">
|
|
86
|
+
{% if rows | length %}
|
|
87
|
+
<table>
|
|
88
|
+
<thead>
|
|
89
|
+
<tr>
|
|
90
|
+
{% for field in listFields %}
|
|
91
|
+
<th class="{% if field.name in sortable %}sortable{% endif %} {% if sort == field.name %}sort-{{ order }}{% endif %}">
|
|
92
|
+
{% if field.name in sortable %}
|
|
93
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}?sort={{ field.name }}&order={{ 'desc' if (sort == field.name and order == 'asc') else 'asc' }}&search={{ search }}&page={{ page }}" style="color:inherit;text-decoration:none">
|
|
94
|
+
{{ field.label }}
|
|
95
|
+
</a>
|
|
96
|
+
{% else %}
|
|
97
|
+
{{ field.label }}
|
|
98
|
+
{% endif %}
|
|
99
|
+
</th>
|
|
100
|
+
{% endfor %}
|
|
101
|
+
{% if resource.canEdit or resource.canDelete %}
|
|
102
|
+
<th class="col-actions">Actions</th>
|
|
103
|
+
{% endif %}
|
|
104
|
+
</tr>
|
|
105
|
+
</thead>
|
|
106
|
+
<tbody>
|
|
107
|
+
{% for row in rows %}
|
|
108
|
+
<tr>
|
|
109
|
+
{% for field in listFields %}
|
|
110
|
+
<td>{{ row[field.name] | adminCell(field) | safe }}</td>
|
|
111
|
+
{% endfor %}
|
|
112
|
+
{% if resource.canEdit or resource.canDelete %}
|
|
113
|
+
<td class="col-actions">
|
|
114
|
+
<div class="flex items-center gap-2" style="justify-content:flex-end">
|
|
115
|
+
{% if resource.canEdit %}
|
|
116
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/edit" class="btn btn-ghost btn-sm">Edit</a>
|
|
117
|
+
{% endif %}
|
|
118
|
+
{% if resource.canDelete %}
|
|
119
|
+
<button onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/delete', '#{{ row.id }}')" class="btn btn-danger btn-sm">Delete</button>
|
|
120
|
+
{% endif %}
|
|
121
|
+
</div>
|
|
122
|
+
</td>
|
|
123
|
+
{% endif %}
|
|
124
|
+
</tr>
|
|
125
|
+
{% endfor %}
|
|
126
|
+
</tbody>
|
|
127
|
+
</table>
|
|
128
|
+
|
|
129
|
+
{# ── Pagination ── #}
|
|
130
|
+
{% if lastPage > 1 %}
|
|
131
|
+
<div class="pagination">
|
|
132
|
+
<button class="page-btn" {% if page <= 1 %}disabled{% endif %}
|
|
133
|
+
onclick="location.href='{{ adminPrefix }}/{{ resource.slug }}?page={{ page - 1 }}&sort={{ sort }}&order={{ order }}&search={{ search }}'">‹</button>
|
|
134
|
+
{% for p in range(1, lastPage + 1) %}
|
|
135
|
+
{% if p <= 3 or p >= lastPage - 1 or (p >= page - 1 and p <= page + 1) %}
|
|
136
|
+
<button class="page-btn {% if p == page %}active{% endif %}"
|
|
137
|
+
onclick="location.href='{{ adminPrefix }}/{{ resource.slug }}?page={{ p }}&sort={{ sort }}&order={{ order }}&search={{ search }}'">{{ p }}</button>
|
|
138
|
+
{% elif p == 4 and page > 5 %}
|
|
139
|
+
<span style="color:var(--text-muted);padding:0 4px">…</span>
|
|
140
|
+
{% endif %}
|
|
141
|
+
{% endfor %}
|
|
142
|
+
<button class="page-btn" {% if page >= lastPage %}disabled{% endif %}
|
|
143
|
+
onclick="location.href='{{ adminPrefix }}/{{ resource.slug }}?page={{ page + 1 }}&sort={{ sort }}&order={{ order }}&search={{ search }}'">›</button>
|
|
144
|
+
<span class="page-info">
|
|
145
|
+
{{ (page - 1) * perPage + 1 }}–{{ [page * perPage, total] | min }} of {{ total }}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
{% endif %}
|
|
149
|
+
|
|
150
|
+
{% else %}
|
|
151
|
+
<div class="empty-state">
|
|
152
|
+
<div class="empty-icon">{{ resource.icon }}</div>
|
|
153
|
+
<div class="empty-title">No {{ resource.label }} found</div>
|
|
154
|
+
<div class="empty-desc">
|
|
155
|
+
{% if search %}No results for "{{ search }}".{% else %}Get started by creating your first record.{% endif %}
|
|
156
|
+
</div>
|
|
157
|
+
{% if resource.canCreate %}
|
|
158
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/create" class="btn btn-primary">+ New {{ resource.singular }}</a>
|
|
159
|
+
{% endif %}
|
|
160
|
+
</div>
|
|
161
|
+
{% endif %}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
{% endblock %}
|
|
@@ -92,7 +92,38 @@ class Application {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* Bind all routes onto the Express app
|
|
95
|
+
* Bind all routes onto the Express app.
|
|
96
|
+
* Does NOT add 404/error fallbacks — call mountFallbacks() after
|
|
97
|
+
* registering any extra middleware (e.g. Admin panel).
|
|
98
|
+
*
|
|
99
|
+
* Typical usage:
|
|
100
|
+
* app.mountRoutes();
|
|
101
|
+
* Admin.mount(expressApp); // admin routes added here
|
|
102
|
+
* app.mountFallbacks(); // 404 + error handler last
|
|
103
|
+
*
|
|
104
|
+
* Or use app.mount() which does all three in one call.
|
|
105
|
+
*/
|
|
106
|
+
mountRoutes() {
|
|
107
|
+
this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
|
|
108
|
+
this._router.mountRoutes();
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Add 404 + global error handlers. Must be called LAST.
|
|
114
|
+
*/
|
|
115
|
+
mountFallbacks() {
|
|
116
|
+
if (!this._router) {
|
|
117
|
+
this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
|
|
118
|
+
}
|
|
119
|
+
this._router.mountFallbacks();
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Bind routes + add 404/error handlers in one call.
|
|
125
|
+
* Use this when NOT using the Admin panel, or when Admin
|
|
126
|
+
* is mounted before this call.
|
|
96
127
|
*/
|
|
97
128
|
mount() {
|
|
98
129
|
const router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
|
package/src/router/Router.js
CHANGED
|
@@ -25,6 +25,54 @@ class Router {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Bind all registered routes onto the Express app.
|
|
28
|
+
* Does NOT add 404/error handlers — call mountFallbacks() after
|
|
29
|
+
* all other middleware (like Admin) has been registered.
|
|
30
|
+
*/
|
|
31
|
+
mountRoutes() {
|
|
32
|
+
const routes = this._registry.all();
|
|
33
|
+
for (const route of routes) {
|
|
34
|
+
this._bindRoute(route);
|
|
35
|
+
}
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add the 404 + global error handlers.
|
|
41
|
+
* Must be called LAST — after all routes and admin panels.
|
|
42
|
+
*/
|
|
43
|
+
mountFallbacks() {
|
|
44
|
+
// 404 handler
|
|
45
|
+
this._app.use((req, res) => {
|
|
46
|
+
res.status(404).json({
|
|
47
|
+
error: 'Not Found',
|
|
48
|
+
message: `Cannot ${req.method} ${req.path}`,
|
|
49
|
+
status: 404,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Global error handler
|
|
54
|
+
this._app.use((err, req, res, _next) => {
|
|
55
|
+
const status = err.status || err.statusCode || 500;
|
|
56
|
+
const message = err.message || 'Internal Server Error';
|
|
57
|
+
|
|
58
|
+
if (status >= 500 && process.env.NODE_ENV !== 'production') {
|
|
59
|
+
console.error(err.stack);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
res.status(status).json({
|
|
63
|
+
error: status >= 500 ? 'Internal Server Error' : message,
|
|
64
|
+
message,
|
|
65
|
+
status,
|
|
66
|
+
...(err.errors && { errors: err.errors }),
|
|
67
|
+
...(status >= 500 && process.env.NODE_ENV !== 'production' && { stack: err.stack }),
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Bind all registered routes onto the Express app
|
|
75
|
+
* AND add 404/error handlers (original behaviour).
|
|
28
76
|
*/
|
|
29
77
|
mount() {
|
|
30
78
|
const routes = this._registry.all();
|
|
@@ -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
|
`,
|