express-file-cluster 0.2.1 → 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/index.js CHANGED
@@ -1,21 +1,23 @@
1
1
  import {
2
2
  configureAuth
3
- } from "./chunk-DQGWFWBV.js";
3
+ } from "./chunk-BHMFPULA.js";
4
4
  import {
5
5
  registerTask,
6
6
  setEnqueueImpl,
7
7
  taskRegistry
8
- } from "./chunk-Z5YNIVQG.js";
8
+ } from "./chunk-LPZEKFVN.js";
9
9
  import {
10
10
  scanDir
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";
17
18
  import cluster2 from "cluster";
18
19
  import os2 from "os";
20
+ import path2 from "path";
19
21
 
20
22
  // src/router/mount.ts
21
23
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
@@ -25,9 +27,12 @@ function asyncWrap(handler) {
25
27
  };
26
28
  }
27
29
  async function mountRoutes(app, routes) {
30
+ const mounted = [];
28
31
  for (const route of routes) {
29
32
  const mod = await import(route.filePath);
30
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;
31
36
  const implemented = [];
32
37
  const unimplemented = [];
33
38
  for (const method of HTTP_METHODS) {
@@ -52,7 +57,9 @@ async function mountRoutes(app, routes) {
52
57
  }
53
58
  });
54
59
  }
60
+ mounted.push({ ...route, methods: implemented, ...meta !== void 0 && { meta } });
55
61
  }
62
+ return mounted;
56
63
  }
57
64
 
58
65
  // src/cluster/index.ts
@@ -96,9 +103,7 @@ async function loadMongoose() {
96
103
  mongoose = await import("mongoose");
97
104
  return mongoose;
98
105
  } catch {
99
- throw new Error(
100
- "[EFC] mongoose is not installed. Run: npm install mongoose"
101
- );
106
+ throw new Error("[EFC] mongoose is not installed. Run: npm install mongoose");
102
107
  }
103
108
  }
104
109
  async function connectMongo(url) {
@@ -311,7 +316,9 @@ async function initBullMQ(opts) {
311
316
  setEnqueueImpl(async (name, payload) => {
312
317
  const def = taskRegistry.get(name);
313
318
  if (!def) {
314
- throw new Error(`[EFC] Cannot enqueue unknown task: "${name}". Is the task file in tasksDir?`);
319
+ throw new Error(
320
+ `[EFC] Cannot enqueue unknown task: "${name}". Is the task file in tasksDir?`
321
+ );
315
322
  }
316
323
  await queue.add(name, payload, {
317
324
  attempts: def.options.retries ?? 3,
@@ -337,6 +344,568 @@ function parseRedisUrl(url) {
337
344
  }
338
345
  }
339
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,'&amp;').replace(/</g,'&lt;')
653
+ .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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">&quot;' + esc(k) + '&quot;</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
+
340
909
  // src/errors.ts
341
910
  var HttpError = class _HttpError extends Error {
342
911
  statusCode;
@@ -383,12 +952,32 @@ async function ignite(config) {
383
952
  const {
384
953
  port: _port,
385
954
  workers,
386
- apiDir,
387
955
  globalMiddlewares = [],
388
956
  onWorkerReady,
389
957
  onWorkerCrash,
390
958
  onError
391
959
  } = config;
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");
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
+ }
392
981
  const envPort = process.env["PORT"] != null ? Number(process.env["PORT"]) : NaN;
393
982
  const port = _port != null && !Number.isNaN(_port) ? _port : !Number.isNaN(envPort) ? envPort : 3e3;
394
983
  const databaseUrl = config.databaseUrl ?? process.env["DATABASE_URL"];
@@ -439,8 +1028,8 @@ async function ignite(config) {
439
1028
  ...cookieDomain !== void 0 && { cookieDomain }
440
1029
  });
441
1030
  }
442
- if (config.tasksDir) {
443
- await scanTasks(config.tasksDir);
1031
+ if (tasksDir) {
1032
+ await scanTasks(tasksDir);
444
1033
  }
445
1034
  if (config.tasks) {
446
1035
  if (config.tasks.backend === "bullmq") {
@@ -451,7 +1040,16 @@ async function ignite(config) {
451
1040
  }
452
1041
  }
453
1042
  const routes = scanDir(apiDir);
454
- await mountRoutes(app, routes);
1043
+ const apiRouter = express.Router();
1044
+ const mounted = await mountRoutes(apiRouter, routes);
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
+ }
455
1053
  if (onError) {
456
1054
  app.use(onError);
457
1055
  } else {