nitro5 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/.install.js +158 -0
- package/README.md +258 -0
- package/binding.gyp +25 -0
- package/index.js +54 -0
- package/native/nitro5.cc +742 -0
- package/nitro5.config.js +8 -0
- package/package.json +62 -0
- package/public/app.tsx +10 -0
- package/public/index.html +10 -0
- package/public/main.tsx +5 -0
- package/src/app.js +1162 -0
- package/src/cache.js +53 -0
- package/src/dashboard.js +215 -0
- package/src/deps-cache.js +37 -0
- package/src/disk-cache.js +37 -0
- package/src/file-worker.js +19 -0
- package/src/hmr.js +38 -0
- package/src/logger.js +51 -0
- package/src/mime.js +32 -0
- package/src/msg.js +16 -0
- package/src/native.js +75 -0
- package/src/router.js +85 -0
- package/src/stat.js +25 -0
- package/src/static.js +201 -0
- package/src/stats.js +25 -0
- package/src/supervisor.js +249 -0
- package/src/thread-pool.js +107 -0
- package/src/tsc.js +329 -0
- package/src/vite.js +15 -0
- package/src/watcher.js +63 -0
package/src/app.js
ADDED
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { Router } from "./router.js";
|
|
5
|
+
import { MemoryCache } from "./cache.js";
|
|
6
|
+
import { ThreadPool } from "./thread-pool.js";
|
|
7
|
+
import { createStaticServer } from "./static.js";
|
|
8
|
+
import { createNitroViteBridge } from "./vite.js";
|
|
9
|
+
import { parseHttpRequest, nativeReady, getMetrics } from "./native.js";
|
|
10
|
+
import { createLogger } from "./logger.js";
|
|
11
|
+
import { handleSSE, sendEvent } from "./dashboard.js";
|
|
12
|
+
import { incRequest, incError, incConn, decConn, getStats } from "./stats.js";
|
|
13
|
+
|
|
14
|
+
const STATUS_TEXT = {
|
|
15
|
+
200: "OK",
|
|
16
|
+
201: "Created",
|
|
17
|
+
204: "No Content",
|
|
18
|
+
301: "Moved Permanently",
|
|
19
|
+
302: "Found",
|
|
20
|
+
400: "Bad Request",
|
|
21
|
+
401: "Unauthorized",
|
|
22
|
+
403: "Forbidden",
|
|
23
|
+
404: "Not Found",
|
|
24
|
+
500: "Internal Server Error"
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function escapeHtml(value = "") {
|
|
28
|
+
return String(value)
|
|
29
|
+
.replaceAll("&", "&")
|
|
30
|
+
.replaceAll("<", "<")
|
|
31
|
+
.replaceAll(">", ">")
|
|
32
|
+
.replaceAll('"', """)
|
|
33
|
+
.replaceAll("'", "'");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizePathname(input = "/") {
|
|
37
|
+
let p = String(input || "/");
|
|
38
|
+
p = p.split("?")[0].split("#")[0];
|
|
39
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
40
|
+
p = path.posix.normalize(p);
|
|
41
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
42
|
+
return p || "/";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderDashboardPage() {
|
|
46
|
+
return `<!DOCTYPE html>
|
|
47
|
+
<html lang="en">
|
|
48
|
+
<head>
|
|
49
|
+
<meta charset="utf-8"/>
|
|
50
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
51
|
+
<meta name="theme-color" content="#6750A4"/>
|
|
52
|
+
<title>Nitro 5 Dashboard</title>
|
|
53
|
+
<style>
|
|
54
|
+
:root {
|
|
55
|
+
--bg: #0f1115;
|
|
56
|
+
--surface: rgba(28, 30, 38, 0.78);
|
|
57
|
+
--surface-2: rgba(38, 40, 52, 0.92);
|
|
58
|
+
--surface-3: #20222b;
|
|
59
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
60
|
+
--text: #e8eaf0;
|
|
61
|
+
--muted: #a8afc1;
|
|
62
|
+
--primary: #8ab4ff;
|
|
63
|
+
--primary-2: #6750A4;
|
|
64
|
+
--success: #4ade80;
|
|
65
|
+
--error: #fb7185;
|
|
66
|
+
--warning: #fbbf24;
|
|
67
|
+
--shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
|
|
68
|
+
--radius-xl: 28px;
|
|
69
|
+
--radius-lg: 20px;
|
|
70
|
+
--radius-md: 16px;
|
|
71
|
+
--radius-sm: 12px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
* {
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
html, body {
|
|
79
|
+
margin: 0;
|
|
80
|
+
padding: 0;
|
|
81
|
+
min-height: 100%;
|
|
82
|
+
background:
|
|
83
|
+
radial-gradient(circle at top left, rgba(103, 80, 164, 0.28), transparent 30%),
|
|
84
|
+
radial-gradient(circle at top right, rgba(138, 180, 255, 0.18), transparent 28%),
|
|
85
|
+
linear-gradient(180deg, #0b0d12 0%, #10131a 100%);
|
|
86
|
+
color: var(--text);
|
|
87
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
body {
|
|
91
|
+
padding: 24px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.app-shell {
|
|
95
|
+
max-width: 1200px;
|
|
96
|
+
margin: 0 auto;
|
|
97
|
+
display: grid;
|
|
98
|
+
gap: 18px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.hero {
|
|
102
|
+
position: relative;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
border: 1px solid var(--border);
|
|
105
|
+
background: linear-gradient(135deg, rgba(103, 80, 164, 0.25), rgba(138, 180, 255, 0.10));
|
|
106
|
+
backdrop-filter: blur(16px);
|
|
107
|
+
border-radius: var(--radius-xl);
|
|
108
|
+
padding: 24px;
|
|
109
|
+
box-shadow: var(--shadow);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.hero::after {
|
|
113
|
+
content: "";
|
|
114
|
+
position: absolute;
|
|
115
|
+
inset: 0;
|
|
116
|
+
background: linear-gradient(120deg, rgba(255,255,255,0.06), transparent 40%);
|
|
117
|
+
pointer-events: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.hero-top {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: space-between;
|
|
124
|
+
gap: 16px;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
position: relative;
|
|
127
|
+
z-index: 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.brand {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 14px;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.logo {
|
|
138
|
+
width: 52px;
|
|
139
|
+
height: 52px;
|
|
140
|
+
border-radius: 18px;
|
|
141
|
+
display: grid;
|
|
142
|
+
place-items: center;
|
|
143
|
+
background: linear-gradient(135deg, var(--primary-2), var(--primary));
|
|
144
|
+
color: white;
|
|
145
|
+
font-weight: 800;
|
|
146
|
+
box-shadow: 0 10px 24px rgba(103, 80, 164, 0.35);
|
|
147
|
+
flex: 0 0 auto;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.title-wrap {
|
|
151
|
+
min-width: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
h1 {
|
|
155
|
+
margin: 0;
|
|
156
|
+
font-size: clamp(1.5rem, 3vw, 2.4rem);
|
|
157
|
+
letter-spacing: -0.03em;
|
|
158
|
+
line-height: 1.1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.subtitle {
|
|
162
|
+
margin-top: 6px;
|
|
163
|
+
color: var(--muted);
|
|
164
|
+
font-size: 0.98rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.toolbar {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 10px;
|
|
171
|
+
flex-wrap: wrap;
|
|
172
|
+
position: relative;
|
|
173
|
+
z-index: 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.btn {
|
|
177
|
+
appearance: none;
|
|
178
|
+
border: 0;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
border-radius: 999px;
|
|
181
|
+
padding: 12px 18px;
|
|
182
|
+
font-weight: 700;
|
|
183
|
+
color: #111827;
|
|
184
|
+
background: linear-gradient(135deg, #c6dafc, #8ab4ff);
|
|
185
|
+
box-shadow: 0 10px 22px rgba(138, 180, 255, 0.20);
|
|
186
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.btn:hover {
|
|
190
|
+
transform: translateY(-1px);
|
|
191
|
+
box-shadow: 0 14px 28px rgba(138, 180, 255, 0.28);
|
|
192
|
+
filter: brightness(1.03);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.btn:active {
|
|
196
|
+
transform: translateY(0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.btn.secondary {
|
|
200
|
+
color: var(--text);
|
|
201
|
+
background: rgba(255, 255, 255, 0.06);
|
|
202
|
+
border: 1px solid var(--border);
|
|
203
|
+
box-shadow: none;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.status-strip {
|
|
207
|
+
display: flex;
|
|
208
|
+
gap: 12px;
|
|
209
|
+
flex-wrap: wrap;
|
|
210
|
+
margin-top: 18px;
|
|
211
|
+
position: relative;
|
|
212
|
+
z-index: 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.chip {
|
|
216
|
+
display: inline-flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 8px;
|
|
219
|
+
border-radius: 999px;
|
|
220
|
+
padding: 10px 14px;
|
|
221
|
+
background: rgba(255,255,255,0.06);
|
|
222
|
+
border: 1px solid var(--border);
|
|
223
|
+
color: var(--text);
|
|
224
|
+
font-size: 0.92rem;
|
|
225
|
+
backdrop-filter: blur(8px);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.chip strong {
|
|
229
|
+
font-variant-numeric: tabular-nums;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.grid {
|
|
233
|
+
display: grid;
|
|
234
|
+
grid-template-columns: 1.1fr 0.9fr;
|
|
235
|
+
gap: 18px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.card {
|
|
239
|
+
border: 1px solid var(--border);
|
|
240
|
+
background: var(--surface);
|
|
241
|
+
backdrop-filter: blur(16px);
|
|
242
|
+
border-radius: var(--radius-lg);
|
|
243
|
+
box-shadow: var(--shadow);
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.card-header {
|
|
248
|
+
padding: 16px 18px;
|
|
249
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
justify-content: space-between;
|
|
253
|
+
gap: 12px;
|
|
254
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.04), transparent);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.card-title {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
gap: 4px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.card-title h2 {
|
|
264
|
+
margin: 0;
|
|
265
|
+
font-size: 1rem;
|
|
266
|
+
letter-spacing: 0.01em;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.card-title span {
|
|
270
|
+
color: var(--muted);
|
|
271
|
+
font-size: 0.88rem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.card-body {
|
|
275
|
+
padding: 18px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.stats-grid {
|
|
279
|
+
display: grid;
|
|
280
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
281
|
+
gap: 14px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.stat {
|
|
285
|
+
border-radius: 18px;
|
|
286
|
+
background: rgba(255,255,255,0.05);
|
|
287
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
288
|
+
padding: 16px;
|
|
289
|
+
min-height: 92px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.stat-label {
|
|
293
|
+
color: var(--muted);
|
|
294
|
+
font-size: 0.88rem;
|
|
295
|
+
margin-bottom: 10px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.stat-value {
|
|
299
|
+
font-size: 1.35rem;
|
|
300
|
+
font-weight: 800;
|
|
301
|
+
letter-spacing: -0.02em;
|
|
302
|
+
font-variant-numeric: tabular-nums;
|
|
303
|
+
word-break: break-word;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.stat-note {
|
|
307
|
+
margin-top: 8px;
|
|
308
|
+
color: var(--muted);
|
|
309
|
+
font-size: 0.84rem;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.log {
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-direction: column;
|
|
315
|
+
gap: 10px;
|
|
316
|
+
max-height: 68vh;
|
|
317
|
+
overflow: auto;
|
|
318
|
+
padding-right: 4px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.log::-webkit-scrollbar {
|
|
322
|
+
width: 10px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.log::-webkit-scrollbar-thumb {
|
|
326
|
+
background: rgba(255,255,255,0.12);
|
|
327
|
+
border-radius: 999px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.log-item {
|
|
331
|
+
border-radius: 16px;
|
|
332
|
+
padding: 12px 14px;
|
|
333
|
+
background: rgba(255,255,255,0.05);
|
|
334
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
335
|
+
color: var(--text);
|
|
336
|
+
font-size: 0.92rem;
|
|
337
|
+
line-height: 1.45;
|
|
338
|
+
display: flex;
|
|
339
|
+
gap: 10px;
|
|
340
|
+
align-items: flex-start;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.log-badge {
|
|
344
|
+
flex: 0 0 auto;
|
|
345
|
+
width: 10px;
|
|
346
|
+
height: 10px;
|
|
347
|
+
border-radius: 999px;
|
|
348
|
+
margin-top: 6px;
|
|
349
|
+
background: var(--primary);
|
|
350
|
+
box-shadow: 0 0 0 4px rgba(138, 180, 255, 0.12);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.log-item.access .log-badge {
|
|
354
|
+
background: var(--success);
|
|
355
|
+
box-shadow: 0 0 0 4px rgba(74, 222, 128, 0.12);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.log-item.error .log-badge {
|
|
359
|
+
background: var(--error);
|
|
360
|
+
box-shadow: 0 0 0 4px rgba(251, 113, 133, 0.12);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.log-meta {
|
|
364
|
+
color: var(--muted);
|
|
365
|
+
font-size: 0.82rem;
|
|
366
|
+
margin-top: 2px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.empty-state {
|
|
370
|
+
color: var(--muted);
|
|
371
|
+
text-align: center;
|
|
372
|
+
padding: 24px 10px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@media (max-width: 960px) {
|
|
376
|
+
.grid {
|
|
377
|
+
grid-template-columns: 1fr;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@media (max-width: 640px) {
|
|
382
|
+
body {
|
|
383
|
+
padding: 14px;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.hero, .card {
|
|
387
|
+
border-radius: 20px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.stats-grid {
|
|
391
|
+
grid-template-columns: 1fr;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.btn {
|
|
395
|
+
width: 100%;
|
|
396
|
+
justify-content: center;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.toolbar {
|
|
400
|
+
width: 100%;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
</style>
|
|
404
|
+
</head>
|
|
405
|
+
<body>
|
|
406
|
+
<div class="app-shell">
|
|
407
|
+
<section class="hero">
|
|
408
|
+
<div class="hero-top">
|
|
409
|
+
<div class="brand">
|
|
410
|
+
<div class="logo">N5</div>
|
|
411
|
+
<div class="title-wrap">
|
|
412
|
+
<h1>🔥 Nitro 5 Dashboard</h1>
|
|
413
|
+
<div class="subtitle">Realtime access log, stats, and worker control panel</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<div class="toolbar">
|
|
418
|
+
<button class="btn secondary" onclick="location.reload()">Refresh</button>
|
|
419
|
+
<button class="btn" onclick="restart()">Restart Workers</button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<div class="status-strip">
|
|
424
|
+
<div class="chip">Uptime <strong><span id="uptime">0</span>s</strong></div>
|
|
425
|
+
<div class="chip">Live feed <strong>ON</strong></div>
|
|
426
|
+
<div class="chip">Mode <strong>Dashboard</strong></div>
|
|
427
|
+
</div>
|
|
428
|
+
</section>
|
|
429
|
+
|
|
430
|
+
<section class="grid">
|
|
431
|
+
<article class="card">
|
|
432
|
+
<div class="card-header">
|
|
433
|
+
<div class="card-title">
|
|
434
|
+
<h2>System Stats</h2>
|
|
435
|
+
<span>Updated live from SSE</span>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="card-body">
|
|
439
|
+
<div id="stats" class="stats-grid">
|
|
440
|
+
<div class="stat">
|
|
441
|
+
<div class="stat-label">Waiting for data</div>
|
|
442
|
+
<div class="stat-value">—</div>
|
|
443
|
+
<div class="stat-note">Stats will appear here once the stream connects.</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
</article>
|
|
448
|
+
|
|
449
|
+
<article class="card">
|
|
450
|
+
<div class="card-header">
|
|
451
|
+
<div class="card-title">
|
|
452
|
+
<h2>Event Log</h2>
|
|
453
|
+
<span>Latest events first</span>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="card-body">
|
|
457
|
+
<div class="log" id="log">
|
|
458
|
+
<div class="empty-state">No events yet.</div>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</article>
|
|
462
|
+
</section>
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
<script>
|
|
466
|
+
const log = document.getElementById("log");
|
|
467
|
+
const uptimeEl = document.getElementById("uptime");
|
|
468
|
+
const statsEl = document.getElementById("stats");
|
|
469
|
+
const start = Date.now();
|
|
470
|
+
|
|
471
|
+
setInterval(() => {
|
|
472
|
+
uptimeEl.textContent = Math.floor((Date.now() - start) / 1000);
|
|
473
|
+
}, 1000);
|
|
474
|
+
|
|
475
|
+
const es = new EventSource("/__nitro5/events");
|
|
476
|
+
|
|
477
|
+
function restart() {
|
|
478
|
+
fetch("/__nitro5/restart", { method: "POST" });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function formatValue(value) {
|
|
482
|
+
if (value === null || value === undefined) return "0";
|
|
483
|
+
if (typeof value === "number") return Number.isFinite(value) ? value.toLocaleString() : "0";
|
|
484
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
485
|
+
return String(value);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function setStats(data) {
|
|
489
|
+
statsEl.innerHTML = [
|
|
490
|
+
{
|
|
491
|
+
label: "Requests",
|
|
492
|
+
value: data.stats?.totalRequests ?? 0,
|
|
493
|
+
note: "Total HTTP requests handled"
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
label: "Errors",
|
|
497
|
+
value: data.stats?.totalErrors ?? 0,
|
|
498
|
+
note: "Captured server errors"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
label: "Connections",
|
|
502
|
+
value: data.stats?.activeConnections ?? 0,
|
|
503
|
+
note: "Open socket connections"
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
label: "RAM",
|
|
507
|
+
value: (data.metrics?.memoryKB ?? 0) + " KB",
|
|
508
|
+
note: "Approx memory usage"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
label: "CPU User",
|
|
512
|
+
value: typeof data.metrics?.cpuUser === "number" ? data.metrics.cpuUser.toFixed(2) : "0",
|
|
513
|
+
note: "User CPU time"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
label: "CPU Sys",
|
|
517
|
+
value: typeof data.metrics?.cpuSystem === "number" ? data.metrics.cpuSystem.toFixed(2) : "0",
|
|
518
|
+
note: "System CPU time"
|
|
519
|
+
}
|
|
520
|
+
].map(item => \`
|
|
521
|
+
<div class="stat">
|
|
522
|
+
<div class="stat-label">\${item.label}</div>
|
|
523
|
+
<div class="stat-value">\${formatValue(item.value)}</div>
|
|
524
|
+
<div class="stat-note">\${item.note}</div>
|
|
525
|
+
</div>
|
|
526
|
+
\`).join("");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function appendLog(kind, text, meta = "") {
|
|
530
|
+
if (log.querySelector(".empty-state")) {
|
|
531
|
+
log.innerHTML = "";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const div = document.createElement("div");
|
|
535
|
+
div.className = "log-item " + kind;
|
|
536
|
+
div.innerHTML = \`
|
|
537
|
+
<div class="log-badge"></div>
|
|
538
|
+
<div>
|
|
539
|
+
<div>\${text}</div>
|
|
540
|
+
\${meta ? \`<div class="log-meta">\${meta}</div>\` : ""}
|
|
541
|
+
</div>
|
|
542
|
+
\`;
|
|
543
|
+
log.prepend(div);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
es.onmessage = (e) => {
|
|
547
|
+
const data = JSON.parse(e.data);
|
|
548
|
+
|
|
549
|
+
if (data.type === "access") {
|
|
550
|
+
appendLog(
|
|
551
|
+
"access",
|
|
552
|
+
"[ACCESS] " + data.method + " " + data.path + " → " + data.status + " (" + data.time + "ms)",
|
|
553
|
+
new Date(data.ts || Date.now()).toLocaleString()
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (data.type === "error") {
|
|
558
|
+
appendLog(
|
|
559
|
+
"error",
|
|
560
|
+
"[ERROR] " + data.message,
|
|
561
|
+
new Date(data.ts || Date.now()).toLocaleString()
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (data.type === "stats") {
|
|
566
|
+
setStats(data);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
es.onerror = () => {
|
|
571
|
+
appendLog("error", "[STREAM] Connection lost, retrying...");
|
|
572
|
+
};
|
|
573
|
+
</script>
|
|
574
|
+
</body>
|
|
575
|
+
</html>`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function renderErrorPage(status, message, config) {
|
|
579
|
+
if (config.errorPages?.[status]) {
|
|
580
|
+
try {
|
|
581
|
+
return fs.readFileSync(config.errorPages[status], "utf8");
|
|
582
|
+
} catch {}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const safeMessage = escapeHtml(message);
|
|
586
|
+
|
|
587
|
+
return `<!DOCTYPE html>
|
|
588
|
+
<html lang="en">
|
|
589
|
+
<head>
|
|
590
|
+
<meta charset="utf-8"/>
|
|
591
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
592
|
+
<meta name="theme-color" content="#6750A4"/>
|
|
593
|
+
<title>${status} Error</title>
|
|
594
|
+
<style>
|
|
595
|
+
:root {
|
|
596
|
+
--bg: #0f1115;
|
|
597
|
+
--surface: rgba(28, 30, 38, 0.78);
|
|
598
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
599
|
+
--text: #e8eaf0;
|
|
600
|
+
--muted: #a8afc1;
|
|
601
|
+
--primary: #8ab4ff;
|
|
602
|
+
--primary-2: #6750A4;
|
|
603
|
+
--shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
|
|
604
|
+
--radius-xl: 28px;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
* { box-sizing: border-box; }
|
|
608
|
+
|
|
609
|
+
html, body {
|
|
610
|
+
margin: 0;
|
|
611
|
+
min-height: 100%;
|
|
612
|
+
background:
|
|
613
|
+
radial-gradient(circle at top left, rgba(103, 80, 164, 0.28), transparent 30%),
|
|
614
|
+
radial-gradient(circle at top right, rgba(138, 180, 255, 0.18), transparent 28%),
|
|
615
|
+
linear-gradient(180deg, #0b0d12 0%, #10131a 100%);
|
|
616
|
+
color: var(--text);
|
|
617
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
body {
|
|
621
|
+
display: grid;
|
|
622
|
+
place-items: center;
|
|
623
|
+
padding: 24px;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.box {
|
|
627
|
+
width: min(560px, 100%);
|
|
628
|
+
border: 1px solid var(--border);
|
|
629
|
+
background: var(--surface);
|
|
630
|
+
backdrop-filter: blur(16px);
|
|
631
|
+
border-radius: var(--radius-xl);
|
|
632
|
+
box-shadow: var(--shadow);
|
|
633
|
+
padding: 28px;
|
|
634
|
+
text-align: center;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.badge {
|
|
638
|
+
width: 64px;
|
|
639
|
+
height: 64px;
|
|
640
|
+
border-radius: 20px;
|
|
641
|
+
margin: 0 auto 18px;
|
|
642
|
+
display: grid;
|
|
643
|
+
place-items: center;
|
|
644
|
+
font-weight: 800;
|
|
645
|
+
font-size: 1.5rem;
|
|
646
|
+
color: white;
|
|
647
|
+
background: linear-gradient(135deg, #cf6679, #6750A4);
|
|
648
|
+
box-shadow: 0 14px 28px rgba(103, 80, 164, 0.28);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
h1 {
|
|
652
|
+
margin: 0;
|
|
653
|
+
font-size: clamp(2rem, 5vw, 3.4rem);
|
|
654
|
+
letter-spacing: -0.04em;
|
|
655
|
+
color: #f3f4f6;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
p {
|
|
659
|
+
margin: 12px 0 0;
|
|
660
|
+
color: var(--muted);
|
|
661
|
+
line-height: 1.6;
|
|
662
|
+
font-size: 1rem;
|
|
663
|
+
word-break: break-word;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.meta {
|
|
667
|
+
margin-top: 18px;
|
|
668
|
+
display: inline-flex;
|
|
669
|
+
align-items: center;
|
|
670
|
+
gap: 10px;
|
|
671
|
+
border-radius: 999px;
|
|
672
|
+
padding: 10px 14px;
|
|
673
|
+
background: rgba(255,255,255,0.06);
|
|
674
|
+
border: 1px solid var(--border);
|
|
675
|
+
color: var(--text);
|
|
676
|
+
font-size: 0.92rem;
|
|
677
|
+
}
|
|
678
|
+
</style>
|
|
679
|
+
</head>
|
|
680
|
+
<body>
|
|
681
|
+
<div class="box">
|
|
682
|
+
<div class="badge">${status}</div>
|
|
683
|
+
<h1>${status} Error</h1>
|
|
684
|
+
<p>${safeMessage}</p>
|
|
685
|
+
<div class="meta">Nitro 5 · Material-styled error page</div>
|
|
686
|
+
</div>
|
|
687
|
+
</body>
|
|
688
|
+
</html>`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function buildPacket(statusCode, headers, body, options = {}) {
|
|
692
|
+
const { omitContentLength = false } = options;
|
|
693
|
+
const statusText = STATUS_TEXT[statusCode] || "OK";
|
|
694
|
+
const bodyBuffer = Buffer.isBuffer(body)
|
|
695
|
+
? body
|
|
696
|
+
: Buffer.from(body ?? "", "utf8");
|
|
697
|
+
|
|
698
|
+
const finalHeaders = {
|
|
699
|
+
Connection: "close",
|
|
700
|
+
...headers
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
if (
|
|
704
|
+
!omitContentLength &&
|
|
705
|
+
!("Content-Length" in finalHeaders) &&
|
|
706
|
+
!("content-length" in finalHeaders)
|
|
707
|
+
) {
|
|
708
|
+
finalHeaders["Content-Length"] = bodyBuffer.length;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const headerLines = Object.entries(finalHeaders)
|
|
712
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
713
|
+
.join("\r\n");
|
|
714
|
+
|
|
715
|
+
const head = `HTTP/1.1 ${statusCode} ${statusText}\r\n${headerLines}\r\n\r\n`;
|
|
716
|
+
|
|
717
|
+
return Buffer.concat([
|
|
718
|
+
Buffer.from(head, "utf8"),
|
|
719
|
+
bodyBuffer
|
|
720
|
+
]);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function scanContentLength(raw) {
|
|
724
|
+
const headerEnd = raw.indexOf("\r\n\r\n");
|
|
725
|
+
if (headerEnd === -1) return 0;
|
|
726
|
+
|
|
727
|
+
const head = raw.slice(0, headerEnd);
|
|
728
|
+
const lines = head.split("\r\n").slice(1);
|
|
729
|
+
|
|
730
|
+
for (const line of lines) {
|
|
731
|
+
const idx = line.indexOf(":");
|
|
732
|
+
if (idx === -1) continue;
|
|
733
|
+
|
|
734
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
735
|
+
const value = line.slice(idx + 1).trim();
|
|
736
|
+
|
|
737
|
+
if (key === "content-length") {
|
|
738
|
+
const n = Number(value);
|
|
739
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function createResponse(socket, config) {
|
|
747
|
+
return {
|
|
748
|
+
socket,
|
|
749
|
+
statusCode: 200,
|
|
750
|
+
headers: {},
|
|
751
|
+
ended: false,
|
|
752
|
+
streaming: false,
|
|
753
|
+
|
|
754
|
+
setHeader(key, value) {
|
|
755
|
+
this.headers[key] = value;
|
|
756
|
+
return this;
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
status(code) {
|
|
760
|
+
this.statusCode = code;
|
|
761
|
+
return this;
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
write(chunk = "") {
|
|
765
|
+
if (this.ended) return this;
|
|
766
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
767
|
+
? chunk
|
|
768
|
+
: Buffer.from(String(chunk), "utf8");
|
|
769
|
+
socket.write(buffer);
|
|
770
|
+
return this;
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
end(chunk = "") {
|
|
774
|
+
if (this.ended) return this;
|
|
775
|
+
if (chunk !== undefined && chunk !== null && chunk !== "") {
|
|
776
|
+
this.write(chunk);
|
|
777
|
+
}
|
|
778
|
+
socket.end();
|
|
779
|
+
this.ended = true;
|
|
780
|
+
this.streaming = false;
|
|
781
|
+
return this;
|
|
782
|
+
},
|
|
783
|
+
|
|
784
|
+
stream(statusCode = 200, extraHeaders = {}) {
|
|
785
|
+
if (this.ended) return this;
|
|
786
|
+
|
|
787
|
+
this.statusCode = statusCode;
|
|
788
|
+
this.streaming = true;
|
|
789
|
+
|
|
790
|
+
const headers = this.finalizeHeaders(Buffer.alloc(0), {
|
|
791
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
792
|
+
"Cache-Control": "no-cache, no-transform",
|
|
793
|
+
Connection: "keep-alive",
|
|
794
|
+
"X-Accel-Buffering": "no",
|
|
795
|
+
...extraHeaders
|
|
796
|
+
}, { omitContentLength: true });
|
|
797
|
+
|
|
798
|
+
socket.write(buildPacket(statusCode, headers, Buffer.alloc(0), { omitContentLength: true }));
|
|
799
|
+
return this;
|
|
800
|
+
},
|
|
801
|
+
|
|
802
|
+
error(code = 500, message = "Internal Server Error") {
|
|
803
|
+
if (this.ended) return;
|
|
804
|
+
|
|
805
|
+
this.statusCode = code;
|
|
806
|
+
|
|
807
|
+
const html = renderErrorPage(code, message, config);
|
|
808
|
+
const bodyBuffer = Buffer.from(html, "utf8");
|
|
809
|
+
|
|
810
|
+
const headers = this.finalizeHeaders(bodyBuffer, {
|
|
811
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
socket.write(buildPacket(code, headers, bodyBuffer));
|
|
815
|
+
socket.end();
|
|
816
|
+
this.ended = true;
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
applyCors(headers) {
|
|
820
|
+
if (!config.cors?.enabled) return headers;
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
...headers,
|
|
824
|
+
"Access-Control-Allow-Origin": config.cors.origin || "*",
|
|
825
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
826
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
827
|
+
Vary: "Origin"
|
|
828
|
+
};
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
finalizeHeaders(bodyBuffer, extraHeaders = {}, options = {}) {
|
|
832
|
+
const headers = {
|
|
833
|
+
...this.headers,
|
|
834
|
+
...extraHeaders
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
if (
|
|
838
|
+
!("Content-Type" in headers) &&
|
|
839
|
+
!("content-type" in headers)
|
|
840
|
+
) {
|
|
841
|
+
headers["Content-Type"] = "text/plain; charset=utf-8";
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (!options.omitContentLength) {
|
|
845
|
+
if (
|
|
846
|
+
!("Content-Length" in headers) &&
|
|
847
|
+
!("content-length" in headers)
|
|
848
|
+
) {
|
|
849
|
+
headers["Content-Length"] = bodyBuffer.length;
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
delete headers["Content-Length"];
|
|
853
|
+
delete headers["content-length"];
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return this.applyCors(headers);
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
send(body = "", code = this.statusCode || 200, contentType = "text/plain; charset=utf-8") {
|
|
860
|
+
if (this.ended) return;
|
|
861
|
+
|
|
862
|
+
this.statusCode = code;
|
|
863
|
+
|
|
864
|
+
let payload = body;
|
|
865
|
+
|
|
866
|
+
if (payload !== null && typeof payload === "object" && !Buffer.isBuffer(payload)) {
|
|
867
|
+
payload = JSON.stringify(payload);
|
|
868
|
+
contentType = "application/json; charset=utf-8";
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const bodyBuffer = Buffer.isBuffer(payload)
|
|
872
|
+
? payload
|
|
873
|
+
: Buffer.from(String(payload), "utf8");
|
|
874
|
+
|
|
875
|
+
const headers = this.finalizeHeaders(bodyBuffer, {
|
|
876
|
+
"Content-Type": this.headers["Content-Type"] || contentType
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
socket.write(buildPacket(code, headers, bodyBuffer));
|
|
880
|
+
socket.end();
|
|
881
|
+
this.ended = true;
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
json(obj, code = this.statusCode || 200) {
|
|
885
|
+
this.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
886
|
+
this.send(obj, code, "application/json; charset=utf-8");
|
|
887
|
+
},
|
|
888
|
+
|
|
889
|
+
binary(buffer, code = this.statusCode || 200, contentType = "application/octet-stream") {
|
|
890
|
+
if (this.ended) return;
|
|
891
|
+
|
|
892
|
+
this.statusCode = code;
|
|
893
|
+
|
|
894
|
+
const bodyBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
895
|
+
const headers = this.finalizeHeaders(bodyBuffer, {
|
|
896
|
+
"Content-Type": contentType
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
socket.write(buildPacket(code, headers, bodyBuffer));
|
|
900
|
+
socket.end();
|
|
901
|
+
this.ended = true;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function createViteIfEnabled(config) {
|
|
907
|
+
if (!config.dev?.useVite) return null;
|
|
908
|
+
return await createNitroViteBridge(true);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function emitAccess(req, res, timeMs) {
|
|
912
|
+
try {
|
|
913
|
+
sendEvent({
|
|
914
|
+
type: "access",
|
|
915
|
+
method: req.method,
|
|
916
|
+
path: req.pathname,
|
|
917
|
+
status: res.statusCode ?? 200,
|
|
918
|
+
time: timeMs,
|
|
919
|
+
ts: Date.now()
|
|
920
|
+
});
|
|
921
|
+
} catch {}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function emitError(error) {
|
|
925
|
+
try {
|
|
926
|
+
sendEvent({
|
|
927
|
+
type: "error",
|
|
928
|
+
message: error?.message || String(error),
|
|
929
|
+
ts: Date.now()
|
|
930
|
+
});
|
|
931
|
+
} catch {}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function handleInternalNitroRoute(req, res, config) {
|
|
935
|
+
const pathname = normalizePathname(req.pathname || "/");
|
|
936
|
+
const method = String(req.method || "GET").toUpperCase();
|
|
937
|
+
|
|
938
|
+
if (pathname === "/__nitro5/dashboard") {
|
|
939
|
+
if (!config.dashboard) return false;
|
|
940
|
+
res.send(renderDashboardPage(), 200, "text/html; charset=utf-8");
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (pathname === "/__nitro5/events") {
|
|
945
|
+
if (!config.dashboard) return false;
|
|
946
|
+
handleSSE(req, res);
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (pathname === "/__nitro5/json-message") {
|
|
951
|
+
res.json({
|
|
952
|
+
name: "Nitro 5 Web Server Messages",
|
|
953
|
+
nativeParser: nativeReady,
|
|
954
|
+
ok: true,
|
|
955
|
+
status: 200,
|
|
956
|
+
statusMessage: STATUS_TEXT[200],
|
|
957
|
+
mode: config.dev?.useVite ? "vite" : "static",
|
|
958
|
+
cache: config.cache,
|
|
959
|
+
metrics: typeof getMetrics === "function" ? getMetrics() : null,
|
|
960
|
+
stats: typeof getStats === "function" ? getStats() : null
|
|
961
|
+
});
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (pathname === "/__nitro5/restart" && method === "POST") {
|
|
966
|
+
if (typeof process.send === "function") {
|
|
967
|
+
process.send({ type: "restart", pid: process.pid, ts: Date.now() });
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
res.json({ ok: true, restartRequested: true });
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export async function createApp(config) {
|
|
978
|
+
const router = new Router();
|
|
979
|
+
const middlewares = [];
|
|
980
|
+
const cache = new MemoryCache();
|
|
981
|
+
const filePool = new ThreadPool(1);
|
|
982
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
983
|
+
const vite = await createViteIfEnabled(config);
|
|
984
|
+
const logger = createLogger(config);
|
|
985
|
+
|
|
986
|
+
const serveStatic = createStaticServer({
|
|
987
|
+
publicDir,
|
|
988
|
+
cache,
|
|
989
|
+
cacheConfig: config.cache,
|
|
990
|
+
filePool,
|
|
991
|
+
vite
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
const app = {
|
|
995
|
+
use(fn) {
|
|
996
|
+
middlewares.push(fn);
|
|
997
|
+
},
|
|
998
|
+
|
|
999
|
+
get: router.get.bind(router),
|
|
1000
|
+
post: router.post.bind(router),
|
|
1001
|
+
put: router.put.bind(router),
|
|
1002
|
+
patch: router.patch.bind(router),
|
|
1003
|
+
delete: router.delete.bind(router),
|
|
1004
|
+
|
|
1005
|
+
async listen(port, callback) {
|
|
1006
|
+
const server = net.createServer((socket) => {
|
|
1007
|
+
incConn();
|
|
1008
|
+
socket.setNoDelay(true);
|
|
1009
|
+
socket.setKeepAlive(true);
|
|
1010
|
+
|
|
1011
|
+
socket.on("close", () => {
|
|
1012
|
+
decConn();
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
let rawBuffer = Buffer.alloc(0);
|
|
1016
|
+
let handling = false;
|
|
1017
|
+
|
|
1018
|
+
const processRequest = async (rawRequest) => {
|
|
1019
|
+
if (handling) return;
|
|
1020
|
+
handling = true;
|
|
1021
|
+
|
|
1022
|
+
const start = Date.now();
|
|
1023
|
+
incRequest();
|
|
1024
|
+
|
|
1025
|
+
try {
|
|
1026
|
+
const req = parseHttpRequest(rawRequest);
|
|
1027
|
+
|
|
1028
|
+
req.pathname = normalizePathname(req.pathname || "/");
|
|
1029
|
+
req.fullPath = normalizePathname(req.fullPath || req.pathname);
|
|
1030
|
+
req.method = String(req.method || "GET").toUpperCase();
|
|
1031
|
+
|
|
1032
|
+
const res = createResponse(socket, config);
|
|
1033
|
+
|
|
1034
|
+
if (config.cors?.enabled && req.method === "OPTIONS") {
|
|
1035
|
+
res.status(204).send("", 204, "text/plain; charset=utf-8");
|
|
1036
|
+
emitAccess(req, res, Date.now() - start);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (handleInternalNitroRoute(req, res, config)) {
|
|
1041
|
+
const timeMs = Date.now() - start;
|
|
1042
|
+
logger.access(req, res, timeMs);
|
|
1043
|
+
emitAccess(req, res, timeMs);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
for (const mw of middlewares) {
|
|
1048
|
+
if (res.ended) break;
|
|
1049
|
+
|
|
1050
|
+
if (mw.length >= 3) {
|
|
1051
|
+
await new Promise((resolve, reject) => {
|
|
1052
|
+
try {
|
|
1053
|
+
const next = (err) => {
|
|
1054
|
+
if (err) reject(err);
|
|
1055
|
+
else resolve();
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
const maybe = mw(req, res, next);
|
|
1059
|
+
if (maybe && typeof maybe.then === "function") {
|
|
1060
|
+
maybe.then(resolve).catch(reject);
|
|
1061
|
+
}
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
reject(error);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
} else {
|
|
1067
|
+
await Promise.resolve(mw(req, res));
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (res.ended) {
|
|
1072
|
+
emitAccess(req, res, Date.now() - start);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const routeHandler = router.resolve(req.method, req.pathname);
|
|
1077
|
+
|
|
1078
|
+
if (routeHandler) {
|
|
1079
|
+
await routeHandler(req, res);
|
|
1080
|
+
|
|
1081
|
+
if (!res.ended) {
|
|
1082
|
+
res.send("");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const timeMs = Date.now() - start;
|
|
1086
|
+
logger.access(req, res, timeMs);
|
|
1087
|
+
emitAccess(req, res, timeMs);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const served = await serveStatic(req, res);
|
|
1092
|
+
if (served) {
|
|
1093
|
+
const timeMs = Date.now() - start;
|
|
1094
|
+
logger.access(req, res, timeMs);
|
|
1095
|
+
emitAccess(req, res, timeMs);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const timeMs = Date.now() - start;
|
|
1100
|
+
res.status(404).error(404, "Page Not Found");
|
|
1101
|
+
logger.access(req, res, timeMs);
|
|
1102
|
+
emitAccess(req, res, timeMs);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
incError();
|
|
1105
|
+
logger.error(err, { method: "UNKNOWN", pathname: "UNKNOWN" });
|
|
1106
|
+
emitError(err);
|
|
1107
|
+
|
|
1108
|
+
try {
|
|
1109
|
+
if (!socket.destroyed) {
|
|
1110
|
+
const res = createResponse(socket, config);
|
|
1111
|
+
res.error(500, "Internal Server Error");
|
|
1112
|
+
}
|
|
1113
|
+
} catch {
|
|
1114
|
+
socket.destroy();
|
|
1115
|
+
}
|
|
1116
|
+
} finally {
|
|
1117
|
+
handling = false;
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
socket.on("data", async (chunk) => {
|
|
1122
|
+
rawBuffer = Buffer.concat([rawBuffer, chunk]);
|
|
1123
|
+
|
|
1124
|
+
const raw = rawBuffer.toString("utf8");
|
|
1125
|
+
const headerEnd = raw.indexOf("\r\n\r\n");
|
|
1126
|
+
if (headerEnd === -1) return;
|
|
1127
|
+
|
|
1128
|
+
const contentLength = scanContentLength(raw);
|
|
1129
|
+
const totalNeeded =
|
|
1130
|
+
Buffer.byteLength(raw.slice(0, headerEnd + 4), "utf8") + contentLength;
|
|
1131
|
+
|
|
1132
|
+
if (rawBuffer.length < totalNeeded) return;
|
|
1133
|
+
|
|
1134
|
+
const requestBuffer = rawBuffer.slice(0, totalNeeded);
|
|
1135
|
+
rawBuffer = rawBuffer.slice(totalNeeded);
|
|
1136
|
+
|
|
1137
|
+
await processRequest(requestBuffer.toString("utf8"));
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
socket.on("error", () => {});
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
server.on("clientError", (err, socket) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const html = renderErrorPage(400, "Bad Request", config);
|
|
1146
|
+
socket.end(
|
|
1147
|
+
`HTTP/1.1 400 Bad Request\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: ${Buffer.byteLength(html, "utf8")}\r\n\r\n${html}`
|
|
1148
|
+
);
|
|
1149
|
+
} catch {}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
return new Promise((resolve) => {
|
|
1153
|
+
server.listen(port, () => {
|
|
1154
|
+
callback?.();
|
|
1155
|
+
resolve(server);
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
return app;
|
|
1162
|
+
}
|