@nexa-stack/framework 1.0.0
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/.env.example +46 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/bin/nexa.mjs +41 -0
- package/bin/nexa.ts +334 -0
- package/docs/AI.md +69 -0
- package/docs/ARCHITECTURE.md +74 -0
- package/docs/EXAMPLES.md +114 -0
- package/docs/FRAMEWORK.md +226 -0
- package/docs/LANGUAGE.md +39 -0
- package/docs/README.md +7 -0
- package/docs/READY.md +51 -0
- package/docs/REFERENCE.md +255 -0
- package/docs/START.md +98 -0
- package/docs/advanced.md +97 -0
- package/docs/authentication.md +54 -0
- package/docs/cli.md +15 -0
- package/docs/compare.md +51 -0
- package/docs/configuration.md +65 -0
- package/docs/database.md +396 -0
- package/docs/installation.md +57 -0
- package/docs/localization.md +47 -0
- package/docs/resources.md +75 -0
- package/docs/routing.md +59 -0
- package/docs/seeding.md +33 -0
- package/docs/services.md +146 -0
- package/package.json +77 -0
- package/packages/auth/src/auth.test.ts +23 -0
- package/packages/auth/src/auth.ts +287 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/cache/src/index.ts +203 -0
- package/packages/client/src/index.ts +49 -0
- package/packages/core/src/app.ts +97 -0
- package/packages/core/src/config.ts +55 -0
- package/packages/core/src/dev.ts +104 -0
- package/packages/core/src/fields.test.ts +81 -0
- package/packages/core/src/fields.ts +309 -0
- package/packages/core/src/index.ts +60 -0
- package/packages/core/src/lang.ts +79 -0
- package/packages/core/src/loader.ts +42 -0
- package/packages/core/src/migrate.ts +91 -0
- package/packages/core/src/policy.ts +36 -0
- package/packages/core/src/registry.ts +17 -0
- package/packages/core/src/reload.ts +92 -0
- package/packages/core/src/resource.test.ts +32 -0
- package/packages/core/src/resource.ts +87 -0
- package/packages/core/src/routes.ts +22 -0
- package/packages/core/src/runtime.ts +11 -0
- package/packages/database/src/builder.ts +266 -0
- package/packages/database/src/database.ts +252 -0
- package/packages/database/src/dialect.ts +186 -0
- package/packages/database/src/index.ts +6 -0
- package/packages/database/src/mysql.ts +114 -0
- package/packages/database/src/postgres.ts +117 -0
- package/packages/database/src/query.ts +115 -0
- package/packages/database/src/sqlite.ts +216 -0
- package/packages/database/src/types.ts +104 -0
- package/packages/events/src/index.ts +17 -0
- package/packages/export/src/index.ts +36 -0
- package/packages/log/src/index.ts +38 -0
- package/packages/mail/src/index.ts +130 -0
- package/packages/notifications/src/index.ts +84 -0
- package/packages/plugins/src/index.ts +39 -0
- package/packages/queue/src/index.ts +185 -0
- package/packages/queue/src/jobs.ts +9 -0
- package/packages/schedule/src/index.ts +64 -0
- package/packages/server/src/index.ts +1 -0
- package/packages/server/src/middleware.ts +143 -0
- package/packages/server/src/query.ts +40 -0
- package/packages/server/src/router.ts +813 -0
- package/packages/sms/src/index.ts +33 -0
- package/packages/storage/src/upload.ts +36 -0
- package/packages/testing/src/index.ts +67 -0
- package/packages/validation/src/index.ts +1 -0
- package/packages/validation/src/validate.test.ts +35 -0
- package/packages/validation/src/validate.ts +112 -0
- package/public/admin.html +369 -0
- package/public/compare.html +66 -0
- package/public/dev-bar.js +213 -0
- package/public/docs.html +315 -0
- package/public/index.html +66 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" dir="ltr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Nexa vs Laravel</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600;700&family=IBM+Plex+Mono:wght@400&display=swap" rel="stylesheet" />
|
|
8
|
+
<style>
|
|
9
|
+
:root { --bg:#f4f1eb; --ink:#161614; --muted:#5e5b54; --nexa:#0f5c4c; --laravel:#c2410c; --panel:#fffcf7; --line:#d9d3c6; }
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
body { margin:0; font-family:"IBM Plex Sans",system-ui,sans-serif; background:var(--bg); color:var(--ink); padding:2rem 1.25rem 4rem; }
|
|
12
|
+
.wrap { max-width:820px; margin:0 auto; }
|
|
13
|
+
a { color:var(--nexa); }
|
|
14
|
+
.nav { display:flex; gap:.6rem; margin-bottom:1.5rem; flex-wrap:wrap; }
|
|
15
|
+
.nav a { text-decoration:none; border:1px solid var(--line); background:var(--panel); padding:.35rem .8rem; border-radius:6px; font-size:.88rem; }
|
|
16
|
+
h1 { font-size:1.8rem; margin:0 0 .5rem; }
|
|
17
|
+
.lead { color:var(--muted); margin:0 0 2rem; }
|
|
18
|
+
h2 { font-size:1.1rem; margin:2rem 0 .75rem; }
|
|
19
|
+
table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; font-size:.92rem; }
|
|
20
|
+
th,td { padding:.7rem .85rem; text-align:left; border-bottom:1px solid var(--line); }
|
|
21
|
+
th { background:#efeae1; }
|
|
22
|
+
tr:last-child td { border-bottom:none; }
|
|
23
|
+
code { font-family:"IBM Plex Mono",monospace; font-size:.82em; direction:ltr; display:inline-block; }
|
|
24
|
+
.n { color:var(--nexa); font-weight:600; }
|
|
25
|
+
.l { color:var(--laravel); font-weight:600; }
|
|
26
|
+
pre { background:#1c1c1a; color:#e8e4dc; padding:1rem; border-radius:8px; direction:ltr; text-align:left; overflow:auto; font-size:.85rem; font-family:"IBM Plex Mono",monospace; }
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="wrap">
|
|
31
|
+
<div class="nav">
|
|
32
|
+
<a href="/docs">Docs</a>
|
|
33
|
+
<a href="/compare">Compare</a>
|
|
34
|
+
<a href="/admin">Admin</a>
|
|
35
|
+
</div>
|
|
36
|
+
<h1>Nexa vs Laravel — same jobs, less code</h1>
|
|
37
|
+
<p class="lead">Ship a full backend in hours, not weeks.</p>
|
|
38
|
+
|
|
39
|
+
<h2>Commands</h2>
|
|
40
|
+
<table>
|
|
41
|
+
<tr><th>Task</th><th class="l">Laravel</th><th class="n">Nexa</th></tr>
|
|
42
|
+
<tr><td>Serve</td><td><code>php artisan serve</code></td><td><code>nexa serve</code></td></tr>
|
|
43
|
+
<tr><td>New table</td><td><code>make:model -mcr</code></td><td><code>resource(...)</code></td></tr>
|
|
44
|
+
<tr><td>New project</td><td><code>composer create-project</code></td><td><code>nexa new my-app</code></td></tr>
|
|
45
|
+
</table>
|
|
46
|
+
|
|
47
|
+
<h2>Data definition</h2>
|
|
48
|
+
<p class="l">Laravel = Migration + Model + Controller + Route</p>
|
|
49
|
+
<p class="n">Nexa = just this:</p>
|
|
50
|
+
<pre>resource("posts", {
|
|
51
|
+
title: string().required(),
|
|
52
|
+
}).admin();</pre>
|
|
53
|
+
|
|
54
|
+
<h2>Services (same idea)</h2>
|
|
55
|
+
<table>
|
|
56
|
+
<tr><th></th><th class="l">Laravel</th><th class="n">Nexa</th></tr>
|
|
57
|
+
<tr><td>Mail</td><td><code>Mail::</code></td><td><code>mail()</code></td></tr>
|
|
58
|
+
<tr><td>Cache</td><td><code>Cache::</code></td><td><code>cache</code></td></tr>
|
|
59
|
+
<tr><td>Log</td><td><code>Log::</code></td><td><code>log</code></td></tr>
|
|
60
|
+
<tr><td>Queue</td><td><code>dispatch()</code></td><td><code>dispatch()</code></td></tr>
|
|
61
|
+
</table>
|
|
62
|
+
|
|
63
|
+
<p style="margin-top:2rem;color:var(--muted)">Start at <a href="/docs#/START">/docs</a> — full backend in hours.<br/>Admin login: <code>admin@nexa.dev</code> / <code>123456</code></p>
|
|
64
|
+
</div>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (window.__NEXA_DEV_BAR__) return;
|
|
3
|
+
window.__NEXA_DEV_BAR__ = true;
|
|
4
|
+
|
|
5
|
+
const GROUP_COLORS = {
|
|
6
|
+
resource: "#38bdf8",
|
|
7
|
+
custom: "#a78bfa",
|
|
8
|
+
auth: "#fbbf24",
|
|
9
|
+
notifications: "#34d399",
|
|
10
|
+
probe: "#94a3b8",
|
|
11
|
+
dev: "#f472b6",
|
|
12
|
+
api: "#64748b",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const STATUS_COLORS = (s) =>
|
|
16
|
+
s >= 500 ? "#f87171" : s >= 400 ? "#fbbf24" : s >= 300 ? "#94a3b8" : "#4ade80";
|
|
17
|
+
|
|
18
|
+
const style = document.createElement("style");
|
|
19
|
+
style.textContent = `
|
|
20
|
+
body.nexa-dev-open { padding-bottom: 320px !important; }
|
|
21
|
+
body.nexa-dev-bar { padding-bottom: 40px !important; }
|
|
22
|
+
#nexa-dev-bar {
|
|
23
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 99999;
|
|
24
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
25
|
+
font-size: 12px; color: #e2e8f0; pointer-events: auto;
|
|
26
|
+
}
|
|
27
|
+
#nexa-dev-bar * { box-sizing: border-box; }
|
|
28
|
+
#nexa-dev-strip {
|
|
29
|
+
display: flex; align-items: center; gap: 12px; height: 36px;
|
|
30
|
+
padding: 0 14px; background: #0f172a; border-top: 1px solid #334155;
|
|
31
|
+
cursor: pointer; user-select: none;
|
|
32
|
+
}
|
|
33
|
+
#nexa-dev-strip:hover { background: #1e293b; }
|
|
34
|
+
#nexa-dev-badge {
|
|
35
|
+
background: linear-gradient(135deg, #0f5c4c, #2563eb);
|
|
36
|
+
color: #fff; font-weight: 700; padding: 2px 8px; border-radius: 4px;
|
|
37
|
+
letter-spacing: 0.04em;
|
|
38
|
+
}
|
|
39
|
+
#nexa-dev-meta { display: flex; gap: 14px; flex: 1; flex-wrap: wrap; align-items: center; }
|
|
40
|
+
#nexa-dev-meta span { color: #94a3b8; }
|
|
41
|
+
#nexa-dev-meta b { color: #f8fafc; font-weight: 600; }
|
|
42
|
+
#nexa-dev-last { color: #64748b; max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
43
|
+
#nexa-dev-chevron { color: #64748b; transition: transform .2s; }
|
|
44
|
+
#nexa-dev-bar.open #nexa-dev-chevron { transform: rotate(180deg); }
|
|
45
|
+
#nexa-dev-panel {
|
|
46
|
+
display: none; height: 280px; background: #1e293b; border-top: 1px solid #334155;
|
|
47
|
+
grid-template-columns: 1fr 1fr; gap: 0;
|
|
48
|
+
}
|
|
49
|
+
#nexa-dev-bar.open #nexa-dev-panel { display: grid; }
|
|
50
|
+
.nexa-dev-col { overflow: hidden; display: flex; flex-direction: column; border-left: 1px solid #334155; }
|
|
51
|
+
.nexa-dev-col:first-child { border-left: none; }
|
|
52
|
+
.nexa-dev-col h3 {
|
|
53
|
+
margin: 0; padding: 8px 12px; font-size: 11px; text-transform: uppercase;
|
|
54
|
+
letter-spacing: 0.08em; color: #64748b; background: #0f172a;
|
|
55
|
+
}
|
|
56
|
+
.nexa-dev-scroll { overflow: auto; flex: 1; padding: 6px 0; }
|
|
57
|
+
.nexa-dev-row {
|
|
58
|
+
display: flex; align-items: center; gap: 8px; padding: 4px 12px;
|
|
59
|
+
border-bottom: 1px solid #33415522;
|
|
60
|
+
}
|
|
61
|
+
.nexa-dev-row:hover { background: #33415544; }
|
|
62
|
+
.nexa-dev-method {
|
|
63
|
+
min-width: 44px; font-weight: 700; font-size: 10px;
|
|
64
|
+
}
|
|
65
|
+
.nexa-dev-path { flex: 1; color: #cbd5e1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
66
|
+
.nexa-dev-tag {
|
|
67
|
+
font-size: 9px; padding: 1px 5px; border-radius: 3px; text-transform: uppercase;
|
|
68
|
+
background: #334155; color: #94a3b8;
|
|
69
|
+
}
|
|
70
|
+
.nexa-dev-ms { color: #64748b; min-width: 36px; text-align: left; }
|
|
71
|
+
.nexa-dev-status { min-width: 28px; font-weight: 700; text-align: center; }
|
|
72
|
+
#nexa-dev-reload-ok { color: #4ade80; }
|
|
73
|
+
#nexa-dev-reload-err { color: #f87171; }
|
|
74
|
+
#nexa-dev-links a { color: #38bdf8; text-decoration: none; margin-left: 8px; }
|
|
75
|
+
#nexa-dev-links a:hover { text-decoration: underline; }
|
|
76
|
+
`;
|
|
77
|
+
document.head.appendChild(style);
|
|
78
|
+
document.body.classList.add("nexa-dev-bar");
|
|
79
|
+
|
|
80
|
+
const root = document.createElement("div");
|
|
81
|
+
root.id = "nexa-dev-bar";
|
|
82
|
+
root.innerHTML = `
|
|
83
|
+
<div id="nexa-dev-panel">
|
|
84
|
+
<div class="nexa-dev-col">
|
|
85
|
+
<h3>Routes</h3>
|
|
86
|
+
<div class="nexa-dev-scroll" id="nexa-dev-routes"></div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="nexa-dev-col">
|
|
89
|
+
<h3>Recent requests</h3>
|
|
90
|
+
<div class="nexa-dev-scroll" id="nexa-dev-requests"></div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div id="nexa-dev-strip">
|
|
94
|
+
<span id="nexa-dev-badge">NEXA</span>
|
|
95
|
+
<div id="nexa-dev-meta"></div>
|
|
96
|
+
<span id="nexa-dev-last"></span>
|
|
97
|
+
<span id="nexa-dev-links">
|
|
98
|
+
<a href="/api/dev/routes" target="_blank" rel="noopener" onclick="event.stopPropagation()">JSON</a>
|
|
99
|
+
<a href="/api" target="_blank" rel="noopener" onclick="event.stopPropagation()">/api</a>
|
|
100
|
+
</span>
|
|
101
|
+
<span id="nexa-dev-chevron">▲</span>
|
|
102
|
+
</div>
|
|
103
|
+
`;
|
|
104
|
+
document.body.appendChild(root);
|
|
105
|
+
|
|
106
|
+
const meta = document.getElementById("nexa-dev-meta");
|
|
107
|
+
const lastEl = document.getElementById("nexa-dev-last");
|
|
108
|
+
const routesListEl = document.getElementById("nexa-dev-routes");
|
|
109
|
+
const requestsEl = document.getElementById("nexa-dev-requests");
|
|
110
|
+
|
|
111
|
+
let routesLoaded = false;
|
|
112
|
+
let open = false;
|
|
113
|
+
|
|
114
|
+
root.querySelector("#nexa-dev-strip").addEventListener("click", () => {
|
|
115
|
+
open = !open;
|
|
116
|
+
root.classList.toggle("open", open);
|
|
117
|
+
document.body.classList.toggle("nexa-dev-open", open);
|
|
118
|
+
document.body.classList.toggle("nexa-dev-bar", !open);
|
|
119
|
+
if (open && !routesLoaded) loadRoutes();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function renderRoutes(routes) {
|
|
123
|
+
routesListEl.innerHTML = routes
|
|
124
|
+
.map((r) => {
|
|
125
|
+
const color = GROUP_COLORS[r.group] || GROUP_COLORS.api;
|
|
126
|
+
return `<div class="nexa-dev-row">
|
|
127
|
+
<span class="nexa-dev-method" style="color:${color}">${r.method}</span>
|
|
128
|
+
<span class="nexa-dev-path" title="${r.path}">${r.path}</span>
|
|
129
|
+
<span class="nexa-dev-tag" style="color:${color}">${r.group}</span>
|
|
130
|
+
</div>`;
|
|
131
|
+
})
|
|
132
|
+
.join("");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderRequests(requests) {
|
|
136
|
+
if (!requests?.length) {
|
|
137
|
+
requestsEl.innerHTML = `<div class="nexa-dev-row"><span class="nexa-dev-path">No requests yet — use the app.</span></div>`;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
requestsEl.innerHTML = requests
|
|
141
|
+
.map((r) => {
|
|
142
|
+
const sc = STATUS_COLORS(r.status);
|
|
143
|
+
return `<div class="nexa-dev-row">
|
|
144
|
+
<span class="nexa-dev-method" style="color:${sc}">${r.method}</span>
|
|
145
|
+
<span class="nexa-dev-path" title="${r.path}">${r.path}</span>
|
|
146
|
+
<span class="nexa-dev-status" style="color:${sc}">${r.status}</span>
|
|
147
|
+
<span class="nexa-dev-ms">${r.ms}ms</span>
|
|
148
|
+
</div>`;
|
|
149
|
+
})
|
|
150
|
+
.join("");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderStatus(d) {
|
|
154
|
+
const reload = d.last_reload_at
|
|
155
|
+
? d.last_reload_error
|
|
156
|
+
? `<span id="nexa-dev-reload-err">reload failed</span>`
|
|
157
|
+
: `<span id="nexa-dev-reload-ok">reload ${timeAgo(d.last_reload_at)}</span>`
|
|
158
|
+
: `<span>no reload yet</span>`;
|
|
159
|
+
|
|
160
|
+
meta.innerHTML = `
|
|
161
|
+
<span>port <b>${d.port}</b></span>
|
|
162
|
+
<span><b>${d.resources}</b> resources</span>
|
|
163
|
+
<span><b>${d.routes}</b> routes</span>
|
|
164
|
+
<span>hot reload <b>${d.hot_reload ? "ON" : "OFF"}</b></span>
|
|
165
|
+
<span>${reload}</span>
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const last = d.recent_requests?.[0];
|
|
169
|
+
lastEl.textContent = last
|
|
170
|
+
? `${last.method} ${last.path} → ${last.status} (${last.ms}ms)`
|
|
171
|
+
: "waiting for requests…";
|
|
172
|
+
|
|
173
|
+
renderRequests(d.recent_requests);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function timeAgo(iso) {
|
|
177
|
+
const sec = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
|
|
178
|
+
if (sec < 5) return "just now";
|
|
179
|
+
if (sec < 60) return `${sec}s ago`;
|
|
180
|
+
return `${Math.round(sec / 60)}m ago`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function loadRoutes() {
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetch("/api/dev/routes");
|
|
186
|
+
if (!res.ok) return;
|
|
187
|
+
const { data } = await res.json();
|
|
188
|
+
renderRoutes(data.routes || []);
|
|
189
|
+
routesLoaded = true;
|
|
190
|
+
} catch {
|
|
191
|
+
routesListEl.innerHTML = `<div class="nexa-dev-row"><span class="nexa-dev-path">Could not load routes.</span></div>`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function poll() {
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch("/api/dev/status");
|
|
198
|
+
if (res.status === 404) {
|
|
199
|
+
root.remove();
|
|
200
|
+
document.body.classList.remove("nexa-dev-bar", "nexa-dev-open");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!res.ok) return;
|
|
204
|
+
const { data } = await res.json();
|
|
205
|
+
renderStatus(data);
|
|
206
|
+
} catch {
|
|
207
|
+
/* server starting */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
poll();
|
|
212
|
+
setInterval(poll, 2500);
|
|
213
|
+
})();
|
package/public/docs.html
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" dir="ltr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Nexa Docs</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #f6f3ee;
|
|
12
|
+
--ink: #1a1a18;
|
|
13
|
+
--muted: #5c5a55;
|
|
14
|
+
--line: #ddd8ce;
|
|
15
|
+
--panel: #fffdf9;
|
|
16
|
+
--accent: #0f5c4c;
|
|
17
|
+
--code-bg: #1e1e1c;
|
|
18
|
+
--code-fg: #e8e4dc;
|
|
19
|
+
}
|
|
20
|
+
* { box-sizing: border-box; }
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
font-family: "IBM Plex Sans", system-ui, sans-serif;
|
|
24
|
+
background:
|
|
25
|
+
radial-gradient(ellipse 80% 50% at 10% -10%, #dfece7 0%, transparent 55%),
|
|
26
|
+
var(--bg);
|
|
27
|
+
color: var(--ink);
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
}
|
|
30
|
+
.layout {
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: 260px 1fr;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
}
|
|
35
|
+
aside {
|
|
36
|
+
border-right: 1px solid var(--line);
|
|
37
|
+
background: var(--panel);
|
|
38
|
+
padding: 1.5rem 1rem 2rem;
|
|
39
|
+
position: sticky;
|
|
40
|
+
top: 0;
|
|
41
|
+
height: 100vh;
|
|
42
|
+
overflow: auto;
|
|
43
|
+
}
|
|
44
|
+
.brand {
|
|
45
|
+
font-size: 1.35rem;
|
|
46
|
+
font-weight: 700;
|
|
47
|
+
color: var(--accent);
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
display: block;
|
|
50
|
+
margin-bottom: 0.25rem;
|
|
51
|
+
}
|
|
52
|
+
.brand-sub {
|
|
53
|
+
font-size: 0.8rem;
|
|
54
|
+
color: var(--muted);
|
|
55
|
+
margin-bottom: 1.5rem;
|
|
56
|
+
}
|
|
57
|
+
nav a {
|
|
58
|
+
display: block;
|
|
59
|
+
padding: 0.45rem 0.7rem;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
color: var(--ink);
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
font-size: 0.92rem;
|
|
64
|
+
margin-bottom: 2px;
|
|
65
|
+
}
|
|
66
|
+
nav a:hover { background: #efeae2; }
|
|
67
|
+
nav a.active {
|
|
68
|
+
background: var(--accent);
|
|
69
|
+
color: #fff;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
}
|
|
72
|
+
nav .group {
|
|
73
|
+
font-size: 0.7rem;
|
|
74
|
+
text-transform: uppercase;
|
|
75
|
+
letter-spacing: 0.06em;
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
margin: 1rem 0.7rem 0.35rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
}
|
|
80
|
+
main {
|
|
81
|
+
padding: 2rem 2.5rem 4rem;
|
|
82
|
+
max-width: 820px;
|
|
83
|
+
}
|
|
84
|
+
.top {
|
|
85
|
+
display: flex;
|
|
86
|
+
gap: 0.75rem;
|
|
87
|
+
align-items: center;
|
|
88
|
+
margin-bottom: 1.5rem;
|
|
89
|
+
flex-wrap: wrap;
|
|
90
|
+
}
|
|
91
|
+
.top a.btn {
|
|
92
|
+
font-size: 0.85rem;
|
|
93
|
+
color: var(--accent);
|
|
94
|
+
text-decoration: none;
|
|
95
|
+
border: 1px solid var(--line);
|
|
96
|
+
padding: 0.35rem 0.75rem;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
background: var(--panel);
|
|
99
|
+
}
|
|
100
|
+
article h1 { font-size: 1.85rem; margin: 0 0 1rem; line-height: 1.3; }
|
|
101
|
+
article h2 { font-size: 1.25rem; margin: 2rem 0 0.75rem; border-bottom: 1px solid var(--line); padding-bottom: 0.35rem; }
|
|
102
|
+
article h3 { font-size: 1.05rem; margin: 1.4rem 0 0.5rem; }
|
|
103
|
+
article p, article li { line-height: 1.7; color: var(--ink); }
|
|
104
|
+
article p { margin: 0.65rem 0; }
|
|
105
|
+
article a { color: var(--accent); }
|
|
106
|
+
article code {
|
|
107
|
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
|
108
|
+
font-size: 0.86em;
|
|
109
|
+
background: #efeae2;
|
|
110
|
+
padding: 0.1em 0.35em;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
}
|
|
113
|
+
article pre {
|
|
114
|
+
background: var(--code-bg);
|
|
115
|
+
color: var(--code-fg);
|
|
116
|
+
padding: 1rem 1.1rem;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
overflow: auto;
|
|
119
|
+
font-size: 0.84rem;
|
|
120
|
+
line-height: 1.55;
|
|
121
|
+
direction: ltr;
|
|
122
|
+
text-align: left;
|
|
123
|
+
}
|
|
124
|
+
article pre code { background: none; padding: 0; color: inherit; }
|
|
125
|
+
article table {
|
|
126
|
+
width: 100%;
|
|
127
|
+
border-collapse: collapse;
|
|
128
|
+
margin: 1rem 0;
|
|
129
|
+
font-size: 0.92rem;
|
|
130
|
+
}
|
|
131
|
+
article th, article td {
|
|
132
|
+
border: 1px solid var(--line);
|
|
133
|
+
padding: 0.5rem 0.65rem;
|
|
134
|
+
text-align: left;
|
|
135
|
+
}
|
|
136
|
+
article th { background: #efeae2; }
|
|
137
|
+
article blockquote {
|
|
138
|
+
margin: 1rem 0;
|
|
139
|
+
padding: 0.6rem 1rem;
|
|
140
|
+
border-left: 3px solid var(--accent);
|
|
141
|
+
background: #e8f2ef;
|
|
142
|
+
color: var(--muted);
|
|
143
|
+
}
|
|
144
|
+
.loading, .error { color: var(--muted); padding: 2rem 0; }
|
|
145
|
+
@media (max-width: 800px) {
|
|
146
|
+
.layout { grid-template-columns: 1fr; }
|
|
147
|
+
aside {
|
|
148
|
+
position: relative;
|
|
149
|
+
height: auto;
|
|
150
|
+
border-left: none;
|
|
151
|
+
border-bottom: 1px solid var(--line);
|
|
152
|
+
}
|
|
153
|
+
main { padding: 1.25rem; }
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
156
|
+
</head>
|
|
157
|
+
<body>
|
|
158
|
+
<div class="layout">
|
|
159
|
+
<aside>
|
|
160
|
+
<a class="brand" href="/docs">Nexa</a>
|
|
161
|
+
<div class="brand-sub">Docs · v1.0.0</div>
|
|
162
|
+
<nav id="nav"></nav>
|
|
163
|
+
</aside>
|
|
164
|
+
<main>
|
|
165
|
+
<div class="top">
|
|
166
|
+
<a class="btn" href="/admin">Admin</a>
|
|
167
|
+
<a class="btn" href="/compare">Why Nexa?</a>
|
|
168
|
+
<a class="btn" href="/api" target="_blank">API</a>
|
|
169
|
+
</div>
|
|
170
|
+
<article id="content" class="loading">Loading…</article>
|
|
171
|
+
</main>
|
|
172
|
+
</div>
|
|
173
|
+
<script>
|
|
174
|
+
const CHAPTERS = [
|
|
175
|
+
{ group: "Start here", items: [
|
|
176
|
+
{ id: "START", title: "★ Quick start" },
|
|
177
|
+
{ id: "REFERENCE", title: "★ API reference" },
|
|
178
|
+
{ id: "compare", title: "Why Nexa?" },
|
|
179
|
+
]},
|
|
180
|
+
{ group: "Guides", items: [
|
|
181
|
+
{ id: "resources", title: "Resources" },
|
|
182
|
+
{ id: "authentication", title: "Authentication" },
|
|
183
|
+
{ id: "database", title: "Database" },
|
|
184
|
+
{ id: "services", title: "Services" },
|
|
185
|
+
{ id: "advanced", title: "Advanced" },
|
|
186
|
+
{ id: "cli", title: "CLI" },
|
|
187
|
+
]},
|
|
188
|
+
{ group: "Reference", items: [
|
|
189
|
+
{ id: "FRAMEWORK", title: "Framework reference" },
|
|
190
|
+
{ id: "READY", title: "Ready checklist" },
|
|
191
|
+
]},
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
function slugFromHash() {
|
|
195
|
+
const h = location.hash.replace(/^#\/?/, "") || "START";
|
|
196
|
+
return h.replace(/\.md$/, "");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderNav(active) {
|
|
200
|
+
const nav = document.getElementById("nav");
|
|
201
|
+
nav.innerHTML = CHAPTERS.map((g) => {
|
|
202
|
+
const links = g.items.map((i) =>
|
|
203
|
+
`<a href="#/${i.id}" class="${i.id === active ? "active" : ""}">${i.title}</a>`
|
|
204
|
+
).join("");
|
|
205
|
+
return `<div class="group">${g.group}</div>${links}`;
|
|
206
|
+
}).join("");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function escapeHtml(s) {
|
|
210
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function mdToHtml(md) {
|
|
214
|
+
const lines = md.replace(/\r\n/g, "\n").split("\n");
|
|
215
|
+
let html = "";
|
|
216
|
+
let inCode = false;
|
|
217
|
+
let codeLang = "";
|
|
218
|
+
let codeBuf = [];
|
|
219
|
+
let inTable = false;
|
|
220
|
+
let tableRows = [];
|
|
221
|
+
let inList = false;
|
|
222
|
+
|
|
223
|
+
function flushList() {
|
|
224
|
+
if (inList) { html += "</ul>"; inList = false; }
|
|
225
|
+
}
|
|
226
|
+
function flushTable() {
|
|
227
|
+
if (!inTable) return;
|
|
228
|
+
const rows = tableRows.filter((r) => !/^\s*\|?\s*-+/.test(r));
|
|
229
|
+
html += "<table>";
|
|
230
|
+
rows.forEach((row, idx) => {
|
|
231
|
+
const cells = row.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
232
|
+
const tag = idx === 0 ? "th" : "td";
|
|
233
|
+
html += "<tr>" + cells.map((c) => `<${tag}>${inline(c)}</${tag}>`).join("") + "</tr>";
|
|
234
|
+
});
|
|
235
|
+
html += "</table>";
|
|
236
|
+
inTable = false;
|
|
237
|
+
tableRows = [];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function inline(t) {
|
|
241
|
+
return escapeHtml(t)
|
|
242
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
243
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
244
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
if (line.startsWith("```")) {
|
|
249
|
+
flushList(); flushTable();
|
|
250
|
+
if (!inCode) {
|
|
251
|
+
inCode = true;
|
|
252
|
+
codeLang = line.slice(3).trim();
|
|
253
|
+
codeBuf = [];
|
|
254
|
+
} else {
|
|
255
|
+
html += `<pre><code>${escapeHtml(codeBuf.join("\n"))}</code></pre>`;
|
|
256
|
+
inCode = false;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (inCode) { codeBuf.push(line); continue; }
|
|
261
|
+
|
|
262
|
+
if (line.trim().startsWith("|")) {
|
|
263
|
+
flushList();
|
|
264
|
+
inTable = true;
|
|
265
|
+
tableRows.push(line);
|
|
266
|
+
continue;
|
|
267
|
+
} else {
|
|
268
|
+
flushTable();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (/^### /.test(line)) { flushList(); html += `<h3>${inline(line.slice(4))}</h3>`; continue; }
|
|
272
|
+
if (/^## /.test(line)) { flushList(); html += `<h2>${inline(line.slice(3))}</h2>`; continue; }
|
|
273
|
+
if (/^# /.test(line)) { flushList(); html += `<h1>${inline(line.slice(2))}</h1>`; continue; }
|
|
274
|
+
if (/^[-*] /.test(line)) {
|
|
275
|
+
if (!inList) { html += "<ul>"; inList = true; }
|
|
276
|
+
html += `<li>${inline(line.slice(2))}</li>`;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (/^\d+\. /.test(line)) {
|
|
280
|
+
flushList();
|
|
281
|
+
html += `<p>${inline(line)}</p>`;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (line.trim() === "") { flushList(); continue; }
|
|
285
|
+
flushList();
|
|
286
|
+
html += `<p>${inline(line)}</p>`;
|
|
287
|
+
}
|
|
288
|
+
flushList(); flushTable();
|
|
289
|
+
if (inCode) html += `<pre><code>${escapeHtml(codeBuf.join("\n"))}</code></pre>`;
|
|
290
|
+
return html;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function load(id) {
|
|
294
|
+
renderNav(id);
|
|
295
|
+
const el = document.getElementById("content");
|
|
296
|
+
el.className = "loading";
|
|
297
|
+
el.textContent = "Loading…";
|
|
298
|
+
try {
|
|
299
|
+
const res = await fetch(`/api/docs/${id}`);
|
|
300
|
+
if (!res.ok) throw new Error("not found");
|
|
301
|
+
const data = await res.json();
|
|
302
|
+
el.className = "";
|
|
303
|
+
el.innerHTML = mdToHtml(data.content);
|
|
304
|
+
document.title = `${data.title || id} · Nexa Docs`;
|
|
305
|
+
} catch {
|
|
306
|
+
el.className = "error";
|
|
307
|
+
el.textContent = "Page not found.";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
window.addEventListener("hashchange", () => load(slugFromHash()));
|
|
312
|
+
load(slugFromHash());
|
|
313
|
+
</script>
|
|
314
|
+
</body>
|
|
315
|
+
</html>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" dir="ltr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Nexa</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--ink: #121410;
|
|
12
|
+
--muted: #5a5f56;
|
|
13
|
+
--panel: rgba(255, 252, 247, 0.88);
|
|
14
|
+
--line: rgba(18, 20, 16, 0.12);
|
|
15
|
+
--accent: #0d5c4c;
|
|
16
|
+
}
|
|
17
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
18
|
+
body {
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
font-family: "IBM Plex Sans", system-ui, sans-serif;
|
|
21
|
+
color: var(--ink);
|
|
22
|
+
background:
|
|
23
|
+
radial-gradient(ellipse 80% 60% at 10% 20%, rgba(13, 92, 76, 0.18), transparent 55%),
|
|
24
|
+
linear-gradient(165deg, #f3efe6 0%, #e7efe9 45%, #f6f1ea 100%);
|
|
25
|
+
display: grid;
|
|
26
|
+
place-items: center;
|
|
27
|
+
padding: 2rem 1.25rem;
|
|
28
|
+
}
|
|
29
|
+
.shell {
|
|
30
|
+
width: min(720px, 100%);
|
|
31
|
+
background: var(--panel);
|
|
32
|
+
border: 1px solid var(--line);
|
|
33
|
+
border-radius: 20px;
|
|
34
|
+
padding: clamp(1.75rem, 4vw, 2.75rem);
|
|
35
|
+
}
|
|
36
|
+
.brand { font-size: clamp(2rem, 5vw, 2.75rem); font-weight: 700; color: var(--accent); }
|
|
37
|
+
h1 { font-size: 1.35rem; margin: 1rem 0 0.5rem; font-weight: 600; }
|
|
38
|
+
p { color: var(--muted); line-height: 1.6; margin-bottom: 1.25rem; }
|
|
39
|
+
.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
|
|
40
|
+
a.btn {
|
|
41
|
+
display: inline-block; padding: 0.7rem 1.1rem; border-radius: 10px;
|
|
42
|
+
text-decoration: none; font-weight: 600; font-size: 0.95rem;
|
|
43
|
+
}
|
|
44
|
+
a.btn.primary { background: var(--accent); color: #fff; }
|
|
45
|
+
a.btn.ghost { border: 1px solid var(--line); color: var(--ink); }
|
|
46
|
+
code { font-family: "IBM Plex Mono", monospace; font-size: 0.85em; background: #efeae2; padding: 0.1em 0.35em; border-radius: 4px; }
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<div class="shell">
|
|
51
|
+
<div class="brand">Nexa</div>
|
|
52
|
+
<h1>Backend framework for Node.js</h1>
|
|
53
|
+
<p>
|
|
54
|
+
Define a <code>resource</code> and get a database table, REST API, auth, and admin panel.
|
|
55
|
+
Default language is English — set <code>APP_LOCALE=ar</code> for Arabic UI.
|
|
56
|
+
</p>
|
|
57
|
+
<div class="actions">
|
|
58
|
+
<a class="btn primary" href="/admin">Admin</a>
|
|
59
|
+
<a class="btn ghost" href="/docs">Docs</a>
|
|
60
|
+
</div>
|
|
61
|
+
<p style="margin-top:1.5rem;font-size:0.85rem">
|
|
62
|
+
Default login: <code>admin@nexa.dev</code> / <code>123456</code>
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|