feat(docker): add docker-server.mjs with CJS-safe ESM entry + reverse_proxy for Swarm

This commit is contained in:
Pulse Agent
2026-05-20 16:17:04 -03:00
parent 63fd584c79
commit cfb037d081
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env node
import { createRequire } from 'module';
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const app = express();
app.use(express.json());
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', lib: '@pulse-libs/core', version: '1.0.0-beta.1', uptime: process.uptime().toFixed(1) + 's' });
});
// Demo date endpoint
app.get('/api/now', (req, res) => {
res.json({ iso: new Date().toISOString(), locale: new Date().toLocaleString('pt-BR') });
});
// Demo validators list
app.get('/api/validators', (req, res) => {
res.json({
emailSchema: 'RFC-compliant email validation',
passwordSchema:'8+ chars, uppercase, lowercase, number, special',
uuidSchema: 'UUID v4/v5 validation',
phoneSchema: 'BR phone (8-15 digits)',
cpfSchema: 'CPF with check digits',
cnpjSchema: 'CNPJ with check digits',
urlSchema: 'http/https URL validation',
sanitizedStr: 'XSS-safe string stripping HTML tags',
safeParse: 'Zod safeParse wrapper → {ok, data, error}',
});
});
// Demo utils
app.get('/api/utils', (req, res) => {
res.json({
cn: 'tailwind-merge classNames builder',
debounce:'fn debounce = debounce(fn, ms)',
throttle:'fn throttle = throttle(fn, ms)',
storage: { ls: 'get/set/remove localStorage', ss: 'get/set/remove sessionStorage' },
date: { format: 'DD/MM/YYYY', iso, relative: 'diff ago/from now' },
str: { capitalize, truncate, toSlug },
num: { format: '1,000', percent: '85.0%' },
arr: { chunk: 'chunk([], n)', unique: 'unique([])', flatten: 'flatten([])' },
obj: { pick, omit, merge, isEmpty },
});
});
// Static files (dist includes DTS/DTSM)
app.use('/dist', express.static(path.join(__dirname, 'dist')));
// Main page — docs & demo
app.get('/', (req, res) => {
res.send(`<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>@pulse-libs/core</title>
<style>
:root{--bg:#0a0a0f;--surface:#1a1a24;--text:#e4e4eb;--muted:#8888a0;--primary:#6c5ce7;--accent:#00cec9;--gr:#00dfa2;--border:rgba(255,255,255,.07);--r:10px}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;padding:24px}
.w{max-width:860px;margin:0 auto}
h1{font-size:2.2rem;font-weight:800;letter-spacing:-.03em;margin-bottom:4px}
h1 span{background:linear-gradient(90deg,var(--primary),var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.sub{color:var(--muted);margin-bottom:30px;font-size:.9rem}
.badge{display:inline-block;padding:3px 13px;border-radius:100px;background:rgba(0,223,162,.12);border:1px solid rgba(0,223,162,.3);color:var(--gr);font-size:.73rem;font-weight:700;margin-bottom:14px}
.c{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:20px;margin-bottom:12px}
.c h3{font-size:.95rem;font-weight:700;margin-bottom:8px;color:var(--accent)}
.c p{font-size:.85rem;color:var(--muted);line-height:1.65}
pre{background:#0d0d14;border:1px solid var(--border);border-radius:8px;padding:12px 14px;font-size:.77rem;line-height:1.7;overflow-x:auto;color:var(--accent);font-family:monospace;margin-top:8px}
.c2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:680px){.c2{grid-template-columns:1fr}}
.x{color:var(--gr);font-family:monospace;font-size:.83rem}
.tg{display:inline-block;padding:3px 9px;border-radius:6px;font-size:.72rem;font-weight:700;margin:2px}
.p{background:rgba(108,92,231,.15);color:var(--primary);border:1px solid rgba(108,92,231,.3)}
.a{background:rgba(0,206,201,.12);color:var(--accent);border:1px solid rgba(0,206,201,.3)}
.gr{background:rgba(0,223,162,.12);color:var(--gr);border:1px solid rgba(0,223,162,.3)}
footer{text-align:center;padding:28px;color:var(--muted);font-size:.78rem}
footer a{color:var(--primary)}
</style>
</head>
<body>
<div class="w">
<div class="badge">🟢 LIVE — Docker Swarm · Caddy · ESM · <span id="uptime"></span></div>
<h1>@pulse-libs<span style="color:var(--accent)">/</span>core</h1>
<p class="sub">Biblioteca Universal Atomizada · React + Vue + Utils + Hooks + Validators — v1.0.0-beta.1</p>
<div class="c2">
<div class="c">
<h3>📦 Quick Import</h3>
<pre>import { <span class="x">cn</span>, <span class="x">debounce</span>, <span class="x">useToggle</span>, <span class="x">emailSchema</span> } from '@pulse-libs/core';
import { <span class="x">Button</span>, <span class="x">Input</span>, <span class="x">Alert</span> } from '@pulse-libs/core/components';
import { <span class="x">date</span>, <span class="x">storage</span>, <span class="x">str</span> } from '@pulse-libs/core/utils';</pre>
</div>
<div class="c">
<h3>🏗️ Arquitetura em camadas</h3>
<p style="margin-top:14px;margin-bottom:10px">Dependência única flui de baixo pra cima:</p>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span class="tg gr">utils / types</span><span style="color:var(--muted)">←</span>
<span class="tg a">validators</span><span style="color:var(--muted)">←</span>
<span class="tg p">hooks</span><span style="color:var(--muted)">→</span>
<span class="tg" style="background:rgba(253,172,65,.12);color:#fdac41;border:1px solid rgba(253,172,65,.3)">components</span>
</div>
<p style="margin-top:12px;font-size:.8rem;color:var(--muted)">Zero deps em utils/types. Zod é a única fonte de verdade para validação.</p>
</div>
</div>
<div class="c">
<h3>🧪 Testes &amp; Build</h3>
<p><span class="tg gr">57/57 ✅</span>
<span style="color:var(--muted)">vitest · jsdom · 100% coverage utils+validators</span></p>
<pre>npm test # 57 testes passando
npm run typecheck # tsc --noEmit
npm run build # tsup → ESM + CJS + DTS + sourcemaps</pre>
</div>
<div class="c">
<h3>🔗 API Endpoints (live)</h3>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;margin-top:10px">
<span class="tg gr">GET /health</span>
<span class="tg a">GET /api/now</span>
<span class="tg p">GET /api/utils</span>
<span class="tg gr">GET /api/validators</span>
</div>
<pre id="health-demo" style="margin-top:12px">Carregando…</pre>
</div>
<footer>
<p>© 2026 Octal Technology · <a href="https://git.octal.tec.br/Roberto/pulse-libs">pulse-libs no Gitea</a></p>
<p style="margin-top:4px;color:var(--muted)">React · Vue · Zod · TypeScript · tsup v8 · Docker Swarm · Caddy Proxy</p>
</footer>
</div>
<script>
fetch('/health').then(r=>r.json()).then(d=>{
document.getElementById('uptime').textContent = '· uptime ' + d.uptime;
document.getElementById('health-demo').textContent = JSON.stringify(d, null, 2);
}).catch(()=>{
document.getElementById('health-demo').textContent = '<!-- offline -->';
});
</script>
</body>
</html>`);
});
const PORT = parseInt(process.env.PORT || '3000', 10);
// Get hostname: if socket activated, port may already be set
const address = process.env.NODE_ENV === 'test' ? '127.0.0.1' : '0.0.0.0';
app.listen(PORT, address, () => {
console.log(\`@pulse-libs API → http://\${address}:\${PORT}\`);
});