express-file-cluster 0.2.2 → 0.2.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/dist/auth/index.d.cts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/cli/index.cjs +66 -5
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +89 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +597 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +596 -4
- package/dist/index.js.map +1 -1
- package/dist/tasks/index.d.cts +1 -1
- package/dist/tasks/index.d.ts +1 -1
- package/dist/{types-DpegkYh2.d.cts → types-BZehD43d.d.cts} +15 -1
- package/dist/{types-DpegkYh2.d.ts → types-BZehD43d.d.ts} +15 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./chunk-MZPG5HXL.js";
|
|
12
12
|
|
|
13
13
|
// src/index.ts
|
|
14
|
+
import fs2 from "fs";
|
|
14
15
|
import express from "express";
|
|
15
16
|
import cors from "cors";
|
|
16
17
|
import cookieParser from "cookie-parser";
|
|
@@ -26,9 +27,12 @@ function asyncWrap(handler) {
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
async function mountRoutes(app, routes) {
|
|
30
|
+
const mounted = [];
|
|
29
31
|
for (const route of routes) {
|
|
30
32
|
const mod = await import(route.filePath);
|
|
31
33
|
const routeMiddlewares = Array.isArray(mod["middlewares"]) ? mod["middlewares"] : [];
|
|
34
|
+
const raw = mod["meta"];
|
|
35
|
+
const meta = raw != null && typeof raw === "object" && !Array.isArray(raw) ? raw : void 0;
|
|
32
36
|
const implemented = [];
|
|
33
37
|
const unimplemented = [];
|
|
34
38
|
for (const method of HTTP_METHODS) {
|
|
@@ -53,7 +57,9 @@ async function mountRoutes(app, routes) {
|
|
|
53
57
|
}
|
|
54
58
|
});
|
|
55
59
|
}
|
|
60
|
+
mounted.push({ ...route, methods: implemented, ...meta !== void 0 && { meta } });
|
|
56
61
|
}
|
|
62
|
+
return mounted;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
// src/cluster/index.ts
|
|
@@ -338,6 +344,568 @@ function parseRedisUrl(url) {
|
|
|
338
344
|
}
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
// src/dashboard.ts
|
|
348
|
+
var HTTP_STATUS = {
|
|
349
|
+
200: "OK",
|
|
350
|
+
201: "Created",
|
|
351
|
+
202: "Accepted",
|
|
352
|
+
204: "No Content",
|
|
353
|
+
400: "Bad Request",
|
|
354
|
+
401: "Unauthorized",
|
|
355
|
+
403: "Forbidden",
|
|
356
|
+
404: "Not Found",
|
|
357
|
+
405: "Method Not Allowed",
|
|
358
|
+
409: "Conflict",
|
|
359
|
+
422: "Unprocessable Entity",
|
|
360
|
+
500: "Internal Server Error",
|
|
361
|
+
503: "Service Unavailable"
|
|
362
|
+
};
|
|
363
|
+
function generateDashboard(routes, basePath, port, projectName, projectVersion) {
|
|
364
|
+
const safeRoutes = JSON.stringify(
|
|
365
|
+
routes.map((r) => ({ urlPath: r.urlPath, methods: r.methods, params: r.params, meta: r.meta ?? null }))
|
|
366
|
+
).replace(/<\/script>/gi, "<\\/script>");
|
|
367
|
+
const safeHttpStatus = JSON.stringify(HTTP_STATUS);
|
|
368
|
+
return `<!DOCTYPE html>
|
|
369
|
+
<html lang="en">
|
|
370
|
+
<head>
|
|
371
|
+
<meta charset="UTF-8" />
|
|
372
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
373
|
+
<title>${projectName} \u2014 API Reference</title>
|
|
374
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
375
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
376
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
377
|
+
<style>
|
|
378
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
379
|
+
:root {
|
|
380
|
+
--bg: #080b0f;
|
|
381
|
+
--bg-card: #0d1117;
|
|
382
|
+
--bg-raised: #111820;
|
|
383
|
+
--border: #1e2832;
|
|
384
|
+
--border-lit: #263040;
|
|
385
|
+
--text: #e2e8f0;
|
|
386
|
+
--text-dim: #64748b;
|
|
387
|
+
--text-muted: #334155;
|
|
388
|
+
--accent: #F0A030;
|
|
389
|
+
--accent-dim: rgba(240,160,48,.12);
|
|
390
|
+
--accent-glow: rgba(240,160,48,.22);
|
|
391
|
+
--purple: #a78bfa;
|
|
392
|
+
--blue: #60a5fa;
|
|
393
|
+
--orange: #fb923c;
|
|
394
|
+
--green: #7AAE8E;
|
|
395
|
+
--radius: 10px;
|
|
396
|
+
--radius-lg: 16px;
|
|
397
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
398
|
+
--font-sans: 'Inter', sans-serif;
|
|
399
|
+
}
|
|
400
|
+
html { scroll-behavior: smooth; }
|
|
401
|
+
body {
|
|
402
|
+
background: var(--bg); color: var(--text);
|
|
403
|
+
font-family: var(--font-sans); font-size: 16px;
|
|
404
|
+
line-height: 1.6; -webkit-font-smoothing: antialiased;
|
|
405
|
+
position: relative;
|
|
406
|
+
}
|
|
407
|
+
body::before {
|
|
408
|
+
content: ''; position: fixed; inset: 0; z-index: 0;
|
|
409
|
+
background-image:
|
|
410
|
+
linear-gradient(var(--border) 1px, transparent 1px),
|
|
411
|
+
linear-gradient(90deg, var(--border) 1px, transparent 1px);
|
|
412
|
+
background-size: 60px 60px; opacity: .28; pointer-events: none;
|
|
413
|
+
}
|
|
414
|
+
section, footer, .page-hero, .docs-layout { position: relative; z-index: 1; }
|
|
415
|
+
a { text-decoration: none; color: inherit; }
|
|
416
|
+
em { font-style: italic; }
|
|
417
|
+
code {
|
|
418
|
+
font-family: var(--font-mono); font-size: .85em;
|
|
419
|
+
background: var(--bg-raised); border: 1px solid var(--border);
|
|
420
|
+
border-radius: 4px; padding: 1px 6px; color: var(--accent);
|
|
421
|
+
}
|
|
422
|
+
.container { max-width: 1140px; margin: 0 auto; padding: 0 24px; }
|
|
423
|
+
::-webkit-scrollbar { width: 6px; }
|
|
424
|
+
::-webkit-scrollbar-track { background: var(--bg); }
|
|
425
|
+
::-webkit-scrollbar-thumb { background: var(--border-lit); border-radius: 3px; }
|
|
426
|
+
|
|
427
|
+
/* \u2500\u2500 PAGE HERO \u2500\u2500 */
|
|
428
|
+
.page-hero {
|
|
429
|
+
padding: 48px 0 56px; border-bottom: 1px solid var(--border);
|
|
430
|
+
}
|
|
431
|
+
.page-hero .hero-glow {
|
|
432
|
+
position: absolute; top: -40px; left: 10%; width: 500px; height: 350px;
|
|
433
|
+
background: radial-gradient(ellipse, rgba(240,160,48,.1) 0%, transparent 68%);
|
|
434
|
+
pointer-events: none;
|
|
435
|
+
}
|
|
436
|
+
.page-title {
|
|
437
|
+
font-size: clamp(2rem, 5vw, 3.4rem); font-weight: 800;
|
|
438
|
+
line-height: 1.1; letter-spacing: -.02em; margin-bottom: 24px;
|
|
439
|
+
display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
|
|
440
|
+
}
|
|
441
|
+
.project-version {
|
|
442
|
+
font-family: var(--font-mono); font-size: clamp(.85rem, 2vw, 1.1rem);
|
|
443
|
+
font-weight: 500; color: var(--accent);
|
|
444
|
+
background: var(--accent-dim); border: 1px solid rgba(240,160,48,.3);
|
|
445
|
+
border-radius: 6px; padding: 2px 10px; letter-spacing: .5px;
|
|
446
|
+
}
|
|
447
|
+
.hero-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 40px; }
|
|
448
|
+
.base-url-pill {
|
|
449
|
+
display: flex; align-items: center; gap: 8px;
|
|
450
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
451
|
+
border-radius: 7px; padding: 8px 14px;
|
|
452
|
+
}
|
|
453
|
+
.base-url-label { font-size: .72rem; font-family: var(--font-mono);
|
|
454
|
+
color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; }
|
|
455
|
+
.base-url-val { font-family: var(--font-mono); font-size: .8rem; color: var(--accent); }
|
|
456
|
+
.count-pill {
|
|
457
|
+
font-family: var(--font-mono); font-size: .72rem; color: var(--text-dim);
|
|
458
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
459
|
+
border-radius: 7px; padding: 8px 14px;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* \u2500\u2500 TOOLBAR \u2500\u2500 */
|
|
463
|
+
.toolbar {
|
|
464
|
+
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
465
|
+
}
|
|
466
|
+
.method-filters { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
467
|
+
.filter-btn {
|
|
468
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
469
|
+
color: var(--text-dim); font-family: var(--font-mono);
|
|
470
|
+
font-size: .72rem; font-weight: 600; letter-spacing: .5px;
|
|
471
|
+
padding: 6px 12px; border-radius: 6px; cursor: pointer;
|
|
472
|
+
transition: all .15s;
|
|
473
|
+
}
|
|
474
|
+
.filter-btn:hover { border-color: var(--border-lit); color: var(--text); }
|
|
475
|
+
.filter-btn.active { background: var(--accent); color: #0d0d1a; border-color: var(--accent); }
|
|
476
|
+
.filter-btn[data-method="GET"].active { background: rgba(96,165,250,.18); color: #60a5fa; border-color: rgba(96,165,250,.4); }
|
|
477
|
+
.filter-btn[data-method="POST"].active { background: rgba(122,174,142,.18); color: #7AAE8E; border-color: rgba(122,174,142,.4); }
|
|
478
|
+
.filter-btn[data-method="PUT"].active { background: rgba(240,160,48,.18); color: #F0A030; border-color: rgba(240,160,48,.4); }
|
|
479
|
+
.filter-btn[data-method="PATCH"].active { background: rgba(251,191,36,.18); color: #fbbf24; border-color: rgba(251,191,36,.4); }
|
|
480
|
+
.filter-btn[data-method="DELETE"].active { background: rgba(248,113,113,.18); color: #f87171; border-color: rgba(248,113,113,.4); }
|
|
481
|
+
.filter-btn[data-method="HEAD"].active { background: rgba(167,139,250,.18); color: #a78bfa; border-color: rgba(167,139,250,.4); }
|
|
482
|
+
.search-wrap { margin-left: auto; }
|
|
483
|
+
.search-input {
|
|
484
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
485
|
+
color: var(--text); font-family: var(--font-sans); font-size: .85rem;
|
|
486
|
+
padding: 7px 14px; border-radius: 7px; width: 240px;
|
|
487
|
+
outline: none; transition: border-color .2s;
|
|
488
|
+
}
|
|
489
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
490
|
+
.search-input:focus { border-color: var(--border-lit); }
|
|
491
|
+
|
|
492
|
+
/* \u2500\u2500 DOCS LAYOUT \u2500\u2500 */
|
|
493
|
+
.docs-layout {
|
|
494
|
+
display: grid; grid-template-columns: 220px 1fr;
|
|
495
|
+
gap: 48px; padding: 52px 0 80px; max-width: 1140px;
|
|
496
|
+
margin: 0 auto; padding-left: 24px; padding-right: 24px;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* \u2500\u2500 SIDEBAR \u2500\u2500 */
|
|
500
|
+
.docs-sidebar { position: sticky; top: 24px; height: fit-content; }
|
|
501
|
+
.sidebar-section { margin-bottom: 28px; }
|
|
502
|
+
.sidebar-label {
|
|
503
|
+
font-family: var(--font-mono); font-size: .68rem; font-weight: 600;
|
|
504
|
+
color: var(--text-muted); letter-spacing: 1.2px; text-transform: uppercase;
|
|
505
|
+
margin-bottom: 8px; padding: 0 4px;
|
|
506
|
+
}
|
|
507
|
+
.sidebar-link {
|
|
508
|
+
display: flex; align-items: center; gap: 8px;
|
|
509
|
+
padding: 7px 10px; border-radius: 6px;
|
|
510
|
+
border-left: 2px solid transparent;
|
|
511
|
+
font-size: .82rem; color: var(--text-dim); cursor: pointer;
|
|
512
|
+
transition: background .15s, color .15s, border-color .15s;
|
|
513
|
+
text-overflow: ellipsis; overflow: hidden; white-space: nowrap;
|
|
514
|
+
}
|
|
515
|
+
.sidebar-link:hover { background: var(--bg-raised); color: var(--text); }
|
|
516
|
+
.sidebar-link.active { color: var(--accent); border-left-color: var(--accent); background: var(--accent-dim); }
|
|
517
|
+
.sidebar-method-dot {
|
|
518
|
+
width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
|
|
519
|
+
}
|
|
520
|
+
.sidebar-path { font-family: var(--font-mono); font-size: .75rem; }
|
|
521
|
+
|
|
522
|
+
/* \u2500\u2500 ENDPOINT CARDS \u2500\u2500 */
|
|
523
|
+
.endpoint-card {
|
|
524
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
525
|
+
border-radius: var(--radius-lg); margin-bottom: 20px;
|
|
526
|
+
overflow: hidden; transition: border-color .2s;
|
|
527
|
+
animation: fadeUp .45s ease both;
|
|
528
|
+
}
|
|
529
|
+
.endpoint-card:hover { border-color: var(--border-lit); }
|
|
530
|
+
@keyframes fadeUp {
|
|
531
|
+
from { opacity: 0; transform: translateY(16px); }
|
|
532
|
+
to { opacity: 1; transform: translateY(0); }
|
|
533
|
+
}
|
|
534
|
+
@media (prefers-reduced-motion: reduce) {
|
|
535
|
+
.endpoint-card { animation: none; }
|
|
536
|
+
}
|
|
537
|
+
.endpoint-header {
|
|
538
|
+
display: flex; align-items: center; gap: 14px;
|
|
539
|
+
padding: 20px 24px 18px; border-bottom: 1px solid var(--border);
|
|
540
|
+
flex-wrap: wrap;
|
|
541
|
+
}
|
|
542
|
+
.method-badges { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
543
|
+
.method-badge {
|
|
544
|
+
font-family: var(--font-mono); font-size: .7rem; font-weight: 600;
|
|
545
|
+
letter-spacing: .8px; padding: 4px 10px; border-radius: 5px;
|
|
546
|
+
border: 1px solid; text-transform: uppercase;
|
|
547
|
+
}
|
|
548
|
+
.endpoint-path {
|
|
549
|
+
font-family: var(--font-mono); font-size: .9rem; color: var(--text);
|
|
550
|
+
background: none; border: none; padding: 0;
|
|
551
|
+
word-break: break-all;
|
|
552
|
+
}
|
|
553
|
+
.endpoint-body { padding: 18px 24px 24px; }
|
|
554
|
+
.endpoint-desc { font-size: .9rem; color: var(--text-dim); line-height: 1.7; margin-bottom: 20px; }
|
|
555
|
+
|
|
556
|
+
/* \u2500\u2500 CODE BLOCKS \u2500\u2500 */
|
|
557
|
+
.example-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
558
|
+
.code-block-wrap {
|
|
559
|
+
background: var(--bg-raised); border: 1px solid var(--border);
|
|
560
|
+
border-radius: var(--radius); overflow: hidden;
|
|
561
|
+
}
|
|
562
|
+
.code-bar {
|
|
563
|
+
display: flex; align-items: center; gap: 6px;
|
|
564
|
+
padding: 9px 14px; border-bottom: 1px solid var(--border);
|
|
565
|
+
background: var(--bg-card);
|
|
566
|
+
}
|
|
567
|
+
.dot { width: 11px; height: 11px; border-radius: 50%; }
|
|
568
|
+
.dot-r { background: #ff5f57; }
|
|
569
|
+
.dot-y { background: #febc2e; }
|
|
570
|
+
.dot-g { background: #28c840; }
|
|
571
|
+
.code-label { font-family: var(--font-mono); font-size: .68rem; color: var(--text-dim); margin-left: 4px; }
|
|
572
|
+
pre.code-pre {
|
|
573
|
+
font-family: var(--font-mono); font-size: .78rem; line-height: 1.7;
|
|
574
|
+
padding: 16px 18px; overflow-x: auto; tab-size: 2; white-space: pre;
|
|
575
|
+
}
|
|
576
|
+
.c-dim { color: var(--text-muted); }
|
|
577
|
+
.c-green { color: #7AAE8E; }
|
|
578
|
+
.c-blue { color: #60a5fa; }
|
|
579
|
+
.c-orange { color: #fb923c; }
|
|
580
|
+
.c-purple { color: #a78bfa; }
|
|
581
|
+
.c-accent { color: var(--accent); }
|
|
582
|
+
.status-2xx { color: #7AAE8E; }
|
|
583
|
+
.status-4xx { color: #f87171; }
|
|
584
|
+
.status-5xx { color: #fb923c; }
|
|
585
|
+
|
|
586
|
+
/* \u2500\u2500 EMPTY STATE \u2500\u2500 */
|
|
587
|
+
.empty-state {
|
|
588
|
+
text-align: center; padding: 80px 24px;
|
|
589
|
+
color: var(--text-muted); font-size: .9rem;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/* \u2500\u2500 RESPONSIVE \u2500\u2500 */
|
|
593
|
+
@media (max-width: 900px) {
|
|
594
|
+
.docs-layout { grid-template-columns: 1fr; }
|
|
595
|
+
.docs-sidebar { position: static; }
|
|
596
|
+
.example-grid { grid-template-columns: 1fr; }
|
|
597
|
+
.search-wrap { margin-left: 0; width: 100%; }
|
|
598
|
+
.search-input { width: 100%; }
|
|
599
|
+
}
|
|
600
|
+
@media (max-width: 600px) {
|
|
601
|
+
.page-hero { padding: 32px 0 32px; }
|
|
602
|
+
.page-title { font-size: 2rem; }
|
|
603
|
+
}
|
|
604
|
+
</style>
|
|
605
|
+
</head>
|
|
606
|
+
<body>
|
|
607
|
+
|
|
608
|
+
<div class="page-hero">
|
|
609
|
+
<div class="hero-glow"></div>
|
|
610
|
+
<div class="container">
|
|
611
|
+
<h1 class="page-title">${projectName}${projectVersion ? `<span class="project-version">v${projectVersion}</span>` : ""}</h1>
|
|
612
|
+
<div class="hero-meta">
|
|
613
|
+
<div class="base-url-pill">
|
|
614
|
+
<span class="base-url-label">Base</span>
|
|
615
|
+
<span class="base-url-val">http://localhost:${port}${basePath}</span>
|
|
616
|
+
</div>
|
|
617
|
+
<div class="count-pill" id="countPill"></div>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="toolbar">
|
|
620
|
+
<div class="method-filters" id="methodFilters">
|
|
621
|
+
<button class="filter-btn active" data-method="ALL">ALL</button>
|
|
622
|
+
</div>
|
|
623
|
+
<div class="search-wrap">
|
|
624
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search endpoints\u2026" />
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
<div class="docs-layout">
|
|
631
|
+
<aside class="docs-sidebar" id="sidebar"></aside>
|
|
632
|
+
<main id="endpoints"></main>
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
<script>
|
|
636
|
+
const ROUTES = ${safeRoutes};
|
|
637
|
+
const BASE_PATH = ${JSON.stringify(basePath)};
|
|
638
|
+
const HTTP_STATUS = ${safeHttpStatus};
|
|
639
|
+
|
|
640
|
+
const METHOD_STYLE = {
|
|
641
|
+
GET: { bg: 'rgba(96,165,250,.14)', color: '#60a5fa', border: 'rgba(96,165,250,.35)' },
|
|
642
|
+
POST: { bg: 'rgba(122,174,142,.14)', color: '#7AAE8E', border: 'rgba(122,174,142,.35)' },
|
|
643
|
+
PUT: { bg: 'rgba(240,160,48,.14)', color: '#F0A030', border: 'rgba(240,160,48,.35)' },
|
|
644
|
+
PATCH: { bg: 'rgba(251,191,36,.14)', color: '#fbbf24', border: 'rgba(251,191,36,.35)' },
|
|
645
|
+
DELETE: { bg: 'rgba(248,113,113,.14)', color: '#f87171', border: 'rgba(248,113,113,.35)' },
|
|
646
|
+
HEAD: { bg: 'rgba(167,139,250,.14)', color: '#a78bfa', border: 'rgba(167,139,250,.35)' },
|
|
647
|
+
OPTIONS: { bg: 'rgba(148,163,184,.14)', color: '#94a3b8', border: 'rgba(148,163,184,.35)' },
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
function esc(s) {
|
|
651
|
+
return String(s)
|
|
652
|
+
.replace(/&/g,'&').replace(/</g,'<')
|
|
653
|
+
.replace(/>/g,'>').replace(/"/g,'"');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function methodBadge(m) {
|
|
657
|
+
const s = METHOD_STYLE[m] || METHOD_STYLE.GET;
|
|
658
|
+
return '<span class="method-badge" style="background:' + s.bg + ';color:' + s.color + ';border-color:' + s.border + '">' + m + '</span>';
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function joinPath(base, p) {
|
|
662
|
+
return (base.replace(/\\/+$/, '') + p) || '/';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function sidebarDot(method) {
|
|
666
|
+
const s = METHOD_STYLE[method] || METHOD_STYLE.GET;
|
|
667
|
+
return '<span class="sidebar-method-dot" style="background:' + s.color + '"></span>';
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function statusClass(code) {
|
|
671
|
+
if (code >= 200 && code < 300) return 'status-2xx';
|
|
672
|
+
if (code >= 400 && code < 500) return 'status-4xx';
|
|
673
|
+
if (code >= 500) return 'status-5xx';
|
|
674
|
+
return '';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function renderSchema(val, indent) {
|
|
678
|
+
const pad = ' '.repeat(indent || 0);
|
|
679
|
+
if (val === null) return '<span class="c-dim">null</span>';
|
|
680
|
+
if (typeof val === 'string') {
|
|
681
|
+
const isDate = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/.test(val);
|
|
682
|
+
return isDate ? '<span class="c-green">Date</span>' : '<span class="c-purple">String</span>';
|
|
683
|
+
}
|
|
684
|
+
if (typeof val === 'number') return '<span class="c-orange">Number</span>';
|
|
685
|
+
if (typeof val === 'boolean') return '<span class="c-purple">Boolean</span>';
|
|
686
|
+
if (Array.isArray(val)) {
|
|
687
|
+
if (!val.length) return '[]';
|
|
688
|
+
return '[\\n' + pad + ' ' + renderSchema(val[0], (indent || 0) + 1) + '\\n' + pad + ']';
|
|
689
|
+
}
|
|
690
|
+
if (typeof val === 'object') {
|
|
691
|
+
const keys = Object.keys(val);
|
|
692
|
+
if (!keys.length) return '{}';
|
|
693
|
+
const inner = keys.map(function(k) {
|
|
694
|
+
return pad + ' <span class="c-blue">"' + esc(k) + '"</span>: ' + renderSchema(val[k], (indent || 0) + 1);
|
|
695
|
+
}).join(',\\n');
|
|
696
|
+
return '{\\n' + inner + '\\n' + pad + '}';
|
|
697
|
+
}
|
|
698
|
+
return esc(String(val));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function codeBlock(label, content) {
|
|
702
|
+
return '<div class="code-block-wrap">' +
|
|
703
|
+
'<div class="code-bar">' +
|
|
704
|
+
'<span class="dot dot-r"></span>' +
|
|
705
|
+
'<span class="dot dot-y"></span>' +
|
|
706
|
+
'<span class="dot dot-g"></span>' +
|
|
707
|
+
'<span class="code-label">' + esc(label) + '</span>' +
|
|
708
|
+
'</div>' +
|
|
709
|
+
'<pre class="code-pre">' + content + '</pre>' +
|
|
710
|
+
'</div>';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function buildRequestContent(route, example) {
|
|
714
|
+
const req = example || {};
|
|
715
|
+
const method = route.methods[0] || 'GET';
|
|
716
|
+
let path = joinPath(BASE_PATH, route.urlPath);
|
|
717
|
+
if (req.params) {
|
|
718
|
+
for (const [k, v] of Object.entries(req.params)) {
|
|
719
|
+
path = path.replace(':' + k, String(v));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (req.query && Object.keys(req.query).length) {
|
|
723
|
+
path += '?' + new URLSearchParams(req.query).toString();
|
|
724
|
+
}
|
|
725
|
+
let lines = '<span class="c-accent">' + esc(method) + '</span> <span class="c-blue">' + esc(path) + '</span>';
|
|
726
|
+
const headers = { ...(req.headers || {}) };
|
|
727
|
+
if (req.body !== undefined && !headers['Content-Type']) {
|
|
728
|
+
headers['Content-Type'] = 'application/json';
|
|
729
|
+
}
|
|
730
|
+
if (Object.keys(headers).length) {
|
|
731
|
+
lines += '\\n' + Object.entries(headers)
|
|
732
|
+
.map(([k, v]) => '<span class="c-dim">' + esc(k) + ':</span> ' + esc(String(v)))
|
|
733
|
+
.join('\\n');
|
|
734
|
+
}
|
|
735
|
+
if (req.body !== undefined) {
|
|
736
|
+
lines += '\\n\\n' + renderSchema(req.body, 0);
|
|
737
|
+
}
|
|
738
|
+
return lines;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function buildResponseContent(example) {
|
|
742
|
+
const resp = example || {};
|
|
743
|
+
const code = resp.status || 200;
|
|
744
|
+
const text = HTTP_STATUS[code] || 'OK';
|
|
745
|
+
const cls = statusClass(code);
|
|
746
|
+
let lines = 'HTTP/1.1 <span class="' + cls + '">' + code + ' ' + esc(text) + '</span>';
|
|
747
|
+
if (resp.body !== undefined) {
|
|
748
|
+
lines += '\\n<span class="c-dim">Content-Type:</span> application/json\\n\\n';
|
|
749
|
+
lines += renderSchema(resp.body, 0);
|
|
750
|
+
}
|
|
751
|
+
return lines;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function renderCard(route, index) {
|
|
755
|
+
const id = 'ep-' + index;
|
|
756
|
+
const fullPath = joinPath(BASE_PATH, route.urlPath);
|
|
757
|
+
const badges = route.methods.map(methodBadge).join('');
|
|
758
|
+
const meta = route.meta;
|
|
759
|
+
const desc = meta && meta.description
|
|
760
|
+
? '<p class="endpoint-desc">' + esc(meta.description) + '</p>'
|
|
761
|
+
: '';
|
|
762
|
+
let examples = '';
|
|
763
|
+
if (meta) {
|
|
764
|
+
const reqHtml = meta.request !== undefined
|
|
765
|
+
? codeBlock('request', buildRequestContent(route, meta.request))
|
|
766
|
+
: '';
|
|
767
|
+
const respHtml = meta.response !== undefined
|
|
768
|
+
? codeBlock('response', buildResponseContent(meta.response))
|
|
769
|
+
: '';
|
|
770
|
+
if (reqHtml || respHtml) {
|
|
771
|
+
examples = '<div class="example-grid">' + reqHtml + respHtml + '</div>';
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const delay = (index * 0.06).toFixed(2) + 's';
|
|
775
|
+
return '<div class="endpoint-card" id="' + id + '" data-methods="' + route.methods.join(',') + '" data-path="' + esc(fullPath) + '" style="animation-delay:' + delay + '">' +
|
|
776
|
+
'<div class="endpoint-header">' +
|
|
777
|
+
'<div class="method-badges">' + badges + '</div>' +
|
|
778
|
+
'<code class="endpoint-path">' + esc(fullPath) + '</code>' +
|
|
779
|
+
'</div>' +
|
|
780
|
+
'<div class="endpoint-body">' + desc + examples + '</div>' +
|
|
781
|
+
'</div>';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function buildMethodFilters() {
|
|
785
|
+
const allMethods = [...new Set(ROUTES.flatMap(r => r.methods))].sort();
|
|
786
|
+
const filtersEl = document.getElementById('methodFilters');
|
|
787
|
+
allMethods.forEach(m => {
|
|
788
|
+
const btn = document.createElement('button');
|
|
789
|
+
btn.className = 'filter-btn';
|
|
790
|
+
btn.dataset.method = m;
|
|
791
|
+
btn.textContent = m;
|
|
792
|
+
filtersEl.appendChild(btn);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function buildSidebar(routes) {
|
|
797
|
+
const sidebar = document.getElementById('sidebar');
|
|
798
|
+
if (!routes.length) { sidebar.innerHTML = ''; return; }
|
|
799
|
+
|
|
800
|
+
const label = document.createElement('div');
|
|
801
|
+
label.className = 'sidebar-label';
|
|
802
|
+
label.textContent = 'Endpoints';
|
|
803
|
+
sidebar.appendChild(label);
|
|
804
|
+
|
|
805
|
+
routes.forEach((route, i) => {
|
|
806
|
+
const link = document.createElement('div');
|
|
807
|
+
link.className = 'sidebar-link';
|
|
808
|
+
link.dataset.target = 'ep-' + ROUTES.indexOf(route);
|
|
809
|
+
link.innerHTML = sidebarDot(route.methods[0] || 'GET') +
|
|
810
|
+
'<span class="sidebar-path">' + esc(joinPath(BASE_PATH, route.urlPath)) + '</span>';
|
|
811
|
+
link.addEventListener('click', () => {
|
|
812
|
+
const target = document.getElementById(link.dataset.target);
|
|
813
|
+
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
814
|
+
});
|
|
815
|
+
sidebar.appendChild(link);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let activeFilter = 'ALL';
|
|
820
|
+
let searchQuery = '';
|
|
821
|
+
|
|
822
|
+
function applyFilters() {
|
|
823
|
+
const cards = document.querySelectorAll('.endpoint-card');
|
|
824
|
+
let visible = 0;
|
|
825
|
+
cards.forEach(card => {
|
|
826
|
+
const methods = card.dataset.methods.split(',');
|
|
827
|
+
const path = card.dataset.path || '';
|
|
828
|
+
const matchMethod = activeFilter === 'ALL' || methods.includes(activeFilter);
|
|
829
|
+
const matchSearch = !searchQuery || path.toLowerCase().includes(searchQuery);
|
|
830
|
+
const show = matchMethod && matchSearch;
|
|
831
|
+
card.style.display = show ? '' : 'none';
|
|
832
|
+
if (show) visible++;
|
|
833
|
+
});
|
|
834
|
+
document.getElementById('empty-state').style.display = visible ? 'none' : 'block';
|
|
835
|
+
|
|
836
|
+
// Sync sidebar visibility
|
|
837
|
+
document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
838
|
+
const targetId = link.dataset.target;
|
|
839
|
+
const targetCard = document.getElementById(targetId);
|
|
840
|
+
link.style.display = targetCard && targetCard.style.display !== 'none' ? '' : 'none';
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function init() {
|
|
845
|
+
const count = ROUTES.length;
|
|
846
|
+
document.getElementById('countPill').textContent = count + ' endpoint' + (count !== 1 ? 's' : '');
|
|
847
|
+
|
|
848
|
+
buildMethodFilters();
|
|
849
|
+
|
|
850
|
+
const container = document.getElementById('endpoints');
|
|
851
|
+
container.innerHTML = ROUTES.map((r, i) => renderCard(r, i)).join('');
|
|
852
|
+
|
|
853
|
+
// Add empty state after cards
|
|
854
|
+
const emptyEl = document.createElement('div');
|
|
855
|
+
emptyEl.id = 'empty-state';
|
|
856
|
+
emptyEl.className = 'empty-state';
|
|
857
|
+
emptyEl.style.display = 'none';
|
|
858
|
+
emptyEl.innerHTML = '<p>No endpoints match your filter.</p>';
|
|
859
|
+
container.after(emptyEl);
|
|
860
|
+
|
|
861
|
+
buildSidebar(ROUTES);
|
|
862
|
+
|
|
863
|
+
// Method filter clicks
|
|
864
|
+
document.getElementById('methodFilters').addEventListener('click', e => {
|
|
865
|
+
const btn = e.target.closest('.filter-btn');
|
|
866
|
+
if (!btn) return;
|
|
867
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
868
|
+
btn.classList.add('active');
|
|
869
|
+
activeFilter = btn.dataset.method;
|
|
870
|
+
applyFilters();
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// Search
|
|
874
|
+
document.getElementById('searchInput').addEventListener('input', e => {
|
|
875
|
+
searchQuery = e.target.value.trim().toLowerCase();
|
|
876
|
+
applyFilters();
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Scroll spy for sidebar
|
|
880
|
+
const cardEls = document.querySelectorAll('.endpoint-card');
|
|
881
|
+
const sidebarLinks = document.querySelectorAll('.sidebar-link');
|
|
882
|
+
const observer = new IntersectionObserver(entries => {
|
|
883
|
+
entries.forEach(entry => {
|
|
884
|
+
if (entry.isIntersecting) {
|
|
885
|
+
const id = entry.target.id;
|
|
886
|
+
sidebarLinks.forEach(l => l.classList.toggle('active', l.dataset.target === id));
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}, { rootMargin: '-80px 0px -60% 0px' });
|
|
890
|
+
cardEls.forEach(el => observer.observe(el));
|
|
891
|
+
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
try {
|
|
895
|
+
init();
|
|
896
|
+
} catch (e) {
|
|
897
|
+
document.body.insertAdjacentHTML('afterbegin',
|
|
898
|
+
'<div style="position:fixed;top:60px;left:0;right:0;z-index:9999;background:#7f1d1d;color:#fca5a5;padding:16px 24px;font-family:monospace;font-size:13px;border-bottom:1px solid #ef4444">' +
|
|
899
|
+
'<strong>Dashboard JS error:</strong> ' + (e && e.message ? e.message : String(e)) +
|
|
900
|
+
(e && e.stack ? '<pre style="margin-top:8px;white-space:pre-wrap">' + e.stack + '</pre>' : '') +
|
|
901
|
+
'</div>'
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
</script>
|
|
905
|
+
</body>
|
|
906
|
+
</html>`;
|
|
907
|
+
}
|
|
908
|
+
|
|
341
909
|
// src/errors.ts
|
|
342
910
|
var HttpError = class _HttpError extends Error {
|
|
343
911
|
statusCode;
|
|
@@ -389,10 +957,27 @@ async function ignite(config) {
|
|
|
389
957
|
onWorkerCrash,
|
|
390
958
|
onError
|
|
391
959
|
} = config;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
960
|
+
function resolveConventionDir(name) {
|
|
961
|
+
const candidates = [
|
|
962
|
+
...process.argv[1] ? [path2.join(path2.dirname(process.argv[1]), name)] : [],
|
|
963
|
+
path2.join(process.cwd(), "src", name),
|
|
964
|
+
path2.join(process.cwd(), name),
|
|
965
|
+
path2.join(process.cwd(), "dist", name)
|
|
966
|
+
];
|
|
967
|
+
return candidates.find((c) => fs2.existsSync(c)) ?? candidates[0];
|
|
968
|
+
}
|
|
969
|
+
const apiDir = resolveConventionDir("api");
|
|
970
|
+
const tasksDir = resolveConventionDir("tasks");
|
|
395
971
|
const basePath = config.basePath ?? "/v1/api";
|
|
972
|
+
function readProjectMeta() {
|
|
973
|
+
try {
|
|
974
|
+
const raw = fs2.readFileSync(path2.join(process.cwd(), "package.json"), "utf8");
|
|
975
|
+
const pkg = JSON.parse(raw);
|
|
976
|
+
return { name: pkg.name ?? "API", version: pkg.version ?? "" };
|
|
977
|
+
} catch {
|
|
978
|
+
return { name: "API", version: "" };
|
|
979
|
+
}
|
|
980
|
+
}
|
|
396
981
|
const envPort = process.env["PORT"] != null ? Number(process.env["PORT"]) : NaN;
|
|
397
982
|
const port = _port != null && !Number.isNaN(_port) ? _port : !Number.isNaN(envPort) ? envPort : 3e3;
|
|
398
983
|
const databaseUrl = config.databaseUrl ?? process.env["DATABASE_URL"];
|
|
@@ -456,8 +1041,15 @@ async function ignite(config) {
|
|
|
456
1041
|
}
|
|
457
1042
|
const routes = scanDir(apiDir);
|
|
458
1043
|
const apiRouter = express.Router();
|
|
459
|
-
await mountRoutes(apiRouter, routes);
|
|
1044
|
+
const mounted = await mountRoutes(apiRouter, routes);
|
|
460
1045
|
app.use(basePath, apiRouter);
|
|
1046
|
+
if (config.dashboard && process.env["NODE_ENV"] === "development") {
|
|
1047
|
+
app.get("/", (_req, res) => {
|
|
1048
|
+
const { name, version } = readProjectMeta();
|
|
1049
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1050
|
+
res.send(generateDashboard(mounted, basePath, port, name, version));
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
461
1053
|
if (onError) {
|
|
462
1054
|
app.use(onError);
|
|
463
1055
|
} else {
|