@purecore/one-server-4-all 0.1.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/dist/server.js ADDED
@@ -0,0 +1,632 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import fs from "node:fs/promises";
4
+ import { createReadStream, existsSync, statSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { exec } from "node:child_process";
7
+ import os from "node:os";
8
+ import { Watcher } from "./watcher.js";
9
+ import { CertGenerator } from "./cert-generator.js";
10
+ // Script injetado no HTML para ouvir mudanças
11
+ const INJECTED_SCRIPT = `
12
+ <!-- Code injected by auto-server -->
13
+ <script>
14
+ (function() {
15
+ console.log('[hot-server] Connected to hot reload');
16
+ const evtSource = new EventSource('/_hot_server_sse');
17
+ evtSource.onmessage = function(event) {
18
+ try {
19
+ const data = JSON.parse(event.data);
20
+ if (data.type === 'css') {
21
+ console.log('[hot-server] CSS changed, injecting...');
22
+ injectCSS(data.file);
23
+ } else {
24
+ console.log('[hot-server] Reloading...');
25
+ window.location.reload();
26
+ }
27
+ } catch (e) {
28
+ // Fallback para compatibilidade
29
+ if (event.data === 'reload') {
30
+ console.log('[hot-server] Reloading...');
31
+ window.location.reload();
32
+ }
33
+ }
34
+ };
35
+
36
+ function injectCSS(filePath) {
37
+ // Remove timestamp do cache dos links CSS
38
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
39
+ const timestamp = Date.now();
40
+
41
+ links.forEach(link => {
42
+ const href = link.getAttribute('href');
43
+ if (href && href.includes(filePath)) {
44
+ // Força reload do CSS adicionando/removendo timestamp
45
+ const newHref = href.split('?')[0] + '?v=' + timestamp;
46
+ link.setAttribute('href', newHref);
47
+ console.log('[hot-server] CSS injected:', filePath);
48
+ }
49
+ });
50
+ }
51
+
52
+ evtSource.onerror = function() {
53
+ console.log('[hot-server] Disconnected. Retrying...');
54
+ };
55
+ })();
56
+ </script>
57
+ `;
58
+ const MIME_TYPES = {
59
+ // Textos e documentos
60
+ ".html": "text/html",
61
+ ".htm": "text/html",
62
+ ".css": "text/css",
63
+ ".js": "text/javascript",
64
+ ".mjs": "text/javascript",
65
+ ".json": "application/json",
66
+ ".xml": "application/xml",
67
+ ".txt": "text/plain",
68
+ ".md": "text/markdown",
69
+ // Imagens
70
+ ".png": "image/png",
71
+ ".jpg": "image/jpeg",
72
+ ".jpeg": "image/jpeg",
73
+ ".gif": "image/gif",
74
+ ".svg": "image/svg+xml",
75
+ ".ico": "image/x-icon",
76
+ ".webp": "image/webp",
77
+ ".bmp": "image/bmp",
78
+ ".tiff": "image/tiff",
79
+ ".tif": "image/tiff",
80
+ // Vídeos
81
+ ".mp4": "video/mp4",
82
+ ".webm": "video/webm",
83
+ ".ogg": "video/ogg",
84
+ ".avi": "video/x-msvideo",
85
+ ".mov": "video/quicktime",
86
+ ".wmv": "video/x-ms-wmv",
87
+ ".flv": "video/x-flv",
88
+ // Áudios
89
+ ".mp3": "audio/mpeg",
90
+ ".wav": "audio/wav",
91
+ ".oga": "audio/ogg",
92
+ ".aac": "audio/aac",
93
+ ".m4a": "audio/m4a",
94
+ ".opus": "audio/opus",
95
+ // Fontes
96
+ ".woff": "font/woff",
97
+ ".woff2": "font/woff2",
98
+ ".ttf": "font/ttf",
99
+ ".otf": "font/otf",
100
+ ".eot": "application/vnd.ms-fontobject",
101
+ // Aplicações e manifestos
102
+ ".pdf": "application/pdf",
103
+ ".zip": "application/zip",
104
+ ".gzip": "application/gzip",
105
+ ".tar": "application/x-tar",
106
+ ".wasm": "application/wasm",
107
+ ".webmanifest": "application/manifest+json",
108
+ // Outros tipos comuns
109
+ ".csv": "text/csv",
110
+ ".tsv": "text/tab-separated-values",
111
+ ".yaml": "application/x-yaml",
112
+ ".yml": "application/x-yaml",
113
+ ".toml": "application/toml",
114
+ };
115
+ export class HotServer {
116
+ config;
117
+ clients = new Set();
118
+ server;
119
+ watcher;
120
+ isHttps = false;
121
+ startTime = Date.now();
122
+ constructor(config) {
123
+ this.config = config;
124
+ this.watcher = new Watcher(config.root);
125
+ this.isHttps = config.https === "true";
126
+ }
127
+ async handleRequest(req, res) {
128
+ // 1. Endpoint de Hot Reload (SSE)
129
+ if (req.url === "/_hot_server_sse") {
130
+ this.handleSSE(req, res);
131
+ return;
132
+ }
133
+ // 2. Servir Arquivos Estáticos
134
+ const urlRaw = req.url || "/";
135
+ const urlPathOnly = urlRaw.split("?")[0];
136
+ let filePath = path.join(this.config.root, urlPathOnly === "/" ? "index.html" : urlPathOnly);
137
+ // Remove query params
138
+ filePath = filePath.split("?")[0];
139
+ // Se for diretório, tenta index.html
140
+ if (existsSync(filePath) && statSync(filePath).isDirectory()) {
141
+ const indexPath = path.join(filePath, "index.html");
142
+ if (existsSync(indexPath)) {
143
+ filePath = indexPath;
144
+ }
145
+ else {
146
+ // Directory listing (UI preta com cards)
147
+ await this.serveDirectoryListing({
148
+ dirPath: filePath,
149
+ requestPath: urlPathOnly,
150
+ res,
151
+ });
152
+ return;
153
+ }
154
+ }
155
+ if (!existsSync(filePath)) {
156
+ // Fallback: se URL não tem extensão, tenta .html e /index.html
157
+ // Ex: /sobre -> /sobre.html, /sobre/index.html
158
+ const hasExt = path.extname(urlPathOnly) !== "";
159
+ if (!hasExt && urlPathOnly !== "/" && !urlPathOnly.endsWith("/")) {
160
+ const htmlCandidate = path.join(this.config.root, `${urlPathOnly}.html`);
161
+ const dirIndexCandidate = path.join(this.config.root, urlPathOnly, "index.html");
162
+ if (existsSync(htmlCandidate)) {
163
+ filePath = htmlCandidate;
164
+ }
165
+ else if (existsSync(dirIndexCandidate)) {
166
+ filePath = dirIndexCandidate;
167
+ }
168
+ }
169
+ // Suporte SPA: se arquivo não existe, tenta servir index.html
170
+ if (this.config.spa === "true") {
171
+ const indexPath = path.join(this.config.root, "index.html");
172
+ if (existsSync(indexPath)) {
173
+ filePath = indexPath;
174
+ }
175
+ else {
176
+ res.writeHead(404);
177
+ res.end(`File not found: ${req.url}`);
178
+ return;
179
+ }
180
+ }
181
+ else {
182
+ res.writeHead(404);
183
+ res.end(`File not found: ${req.url}`);
184
+ return;
185
+ }
186
+ }
187
+ const ext = path.extname(filePath).toLowerCase();
188
+ const mimeType = MIME_TYPES[ext] || "application/octet-stream";
189
+ // Log detalhado de arquivos servidos
190
+ const relativePath = path.relative(this.config.root, filePath);
191
+ const fileSize = (await fs.stat(filePath)).size;
192
+ console.log(`📄 Servindo: ${relativePath} (${this.formatBytes(fileSize)}) [${mimeType}]`);
193
+ // 3. Injeção de Script (apenas HTML)
194
+ if (ext === ".html") {
195
+ try {
196
+ let content = await fs.readFile(filePath, "utf-8");
197
+ // Analisar e logar recursos do HTML
198
+ this.logHtmlResources(content, relativePath);
199
+ // Injeta antes de </body> ou no final se não tiver body
200
+ if (content.includes("</body>")) {
201
+ content = content.replace("</body>", `${INJECTED_SCRIPT}</body>`);
202
+ }
203
+ else {
204
+ content += INJECTED_SCRIPT;
205
+ }
206
+ res.writeHead(200, {
207
+ "Content-Type": "text/html",
208
+ "Access-Control-Allow-Origin": "*",
209
+ "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
210
+ "Access-Control-Allow-Headers": "Content-Type",
211
+ });
212
+ res.end(content);
213
+ console.log(`🌐 HTML injetado com hot-reload: ${relativePath}`);
214
+ }
215
+ catch (err) {
216
+ res.writeHead(500);
217
+ res.end("Internal Server Error");
218
+ console.error(`❌ Erro ao servir HTML ${relativePath}:`, err);
219
+ }
220
+ }
221
+ else {
222
+ // Stream para arquivos binários ou grandes
223
+ res.writeHead(200, {
224
+ "Content-Type": mimeType,
225
+ "Access-Control-Allow-Origin": "*",
226
+ "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
227
+ "Access-Control-Allow-Headers": "Content-Type",
228
+ });
229
+ const stream = createReadStream(filePath);
230
+ stream.pipe(res);
231
+ }
232
+ }
233
+ handleSSE(req, res) {
234
+ res.writeHead(200, {
235
+ "Content-Type": "text/event-stream",
236
+ "Cache-Control": "no-cache",
237
+ Connection: "keep-alive",
238
+ "Access-Control-Allow-Origin": "*",
239
+ });
240
+ // Mantém conexão viva
241
+ res.write("data: connected\n\n");
242
+ this.clients.add(res);
243
+ req.on("close", () => {
244
+ this.clients.delete(res);
245
+ });
246
+ }
247
+ notifyClients(changedFile) {
248
+ if (this.clients.size === 0)
249
+ return;
250
+ if (changedFile) {
251
+ const ext = path.extname(changedFile).toLowerCase();
252
+ if (ext === ".css") {
253
+ console.log(`📡 Notificando ${this.clients.size} clientes sobre mudança CSS: ${changedFile}`);
254
+ const data = JSON.stringify({ type: "css", file: changedFile });
255
+ for (const client of this.clients) {
256
+ client.write(`data: ${data}\n\n`);
257
+ }
258
+ return;
259
+ }
260
+ }
261
+ // Fallback para reload completo
262
+ console.log(`📡 Notificando ${this.clients.size} clientes para recarregar...`);
263
+ for (const client of this.clients) {
264
+ client.write("data: reload\n\n");
265
+ }
266
+ }
267
+ openBrowser() {
268
+ const protocol = this.isHttps ? "https" : "http";
269
+ const url = `${protocol}://localhost:${this.config.port}`;
270
+ const start = process.platform == "darwin"
271
+ ? "open"
272
+ : process.platform == "win32"
273
+ ? "start"
274
+ : "xdg-open";
275
+ exec(`${start} ${url}`);
276
+ }
277
+ async start() {
278
+ // Inicia Watcher
279
+ this.watcher.on("change", (filePath) => this.notifyClients(filePath));
280
+ this.watcher.start();
281
+ // Criar servidor HTTP ou HTTPS
282
+ if (this.isHttps) {
283
+ const certs = await CertGenerator.getCertPaths();
284
+ if (!certs) {
285
+ console.log("🔐 Gerando certificados auto-assinados...");
286
+ await CertGenerator.generateCerts();
287
+ }
288
+ const certPaths = await CertGenerator.getCertPaths();
289
+ if (certPaths) {
290
+ const options = {
291
+ key: await fs.readFile(certPaths.keyPath),
292
+ cert: await fs.readFile(certPaths.certPath),
293
+ };
294
+ this.server = https.createServer(options, this.handleRequest.bind(this));
295
+ }
296
+ else {
297
+ throw new Error("Não foi possível gerar certificados HTTPS");
298
+ }
299
+ }
300
+ else {
301
+ this.server = http.createServer(this.handleRequest.bind(this));
302
+ }
303
+ const tryListen = (port) => {
304
+ const onError = (err) => {
305
+ if (err.code === "EADDRINUSE") {
306
+ console.log(`\n \x1b[33mPort ${port} is in use, trying another one...\x1b[0m`);
307
+ // Garante que limpamos listeners antigos antes de tentar de novo
308
+ this.server.removeListener("error", onError);
309
+ this.server.close();
310
+ tryListen(port + 1);
311
+ }
312
+ else {
313
+ console.error("❌ Erro ao iniciar servidor:", err);
314
+ process.exit(1);
315
+ }
316
+ };
317
+ this.server.once("error", onError);
318
+ this.server.listen(port, () => {
319
+ this.server.removeListener("error", onError);
320
+ this.config.port = port;
321
+ const protocol = this.isHttps ? "https" : "http";
322
+ const readyTime = Date.now() - this.startTime;
323
+ const version = "0.2.0";
324
+ const cyan = (text) => `\x1b[36m${text}\x1b[0m`;
325
+ const green = (text) => `\x1b[32m${text}\x1b[0m`;
326
+ const bold = (text) => `\x1b[1m${text}\x1b[0m`;
327
+ const gray = (text) => `\x1b[90m${text}\x1b[0m`;
328
+ console.log(`\n ${bold(cyan("HOT-SERVER"))} ${cyan("v" + version)} ${gray("ready in")} ${bold(green(readyTime + " ms"))}\n`);
329
+ console.log(` ${green("➜")} ${bold("Local")}: ${cyan(`${protocol}://localhost:${this.config.port}/`)}`);
330
+ const networks = this.getNetworkIPs();
331
+ networks.forEach((ip) => {
332
+ console.log(` ${green("➜")} ${bold("Network")}: ${cyan(`${protocol}://${ip}:${this.config.port}/`)}`);
333
+ });
334
+ console.log(` ${green("➜")} ${gray("press")} ${bold("h + enter")} ${gray("to show help")}\n`);
335
+ if (this.config.open === "true") {
336
+ this.openBrowser();
337
+ }
338
+ });
339
+ };
340
+ tryListen(this.config.port);
341
+ }
342
+ formatBytes(bytes) {
343
+ if (bytes === 0)
344
+ return "0 B";
345
+ const k = 1024;
346
+ const sizes = ["B", "KB", "MB", "GB"];
347
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
348
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
349
+ }
350
+ logHtmlResources(htmlContent, htmlPath) {
351
+ const resources = [];
352
+ // CSS links
353
+ const cssMatches = htmlContent.match(/<link[^>]*href="([^"]*\.css[^"]*)"[^>]*>/gi);
354
+ if (cssMatches) {
355
+ cssMatches.forEach((match) => {
356
+ const hrefMatch = match.match(/href="([^"]*\.css[^"]*)"/i);
357
+ if (hrefMatch) {
358
+ resources.push({ type: "CSS", path: hrefMatch[1] });
359
+ }
360
+ });
361
+ }
362
+ // JavaScript scripts
363
+ const jsMatches = htmlContent.match(/<script[^>]*src="([^"]*\.js[^"]*)"[^>]*><\/script>/gi);
364
+ if (jsMatches) {
365
+ jsMatches.forEach((match) => {
366
+ const srcMatch = match.match(/src="([^"]*\.js[^"]*)"/i);
367
+ if (srcMatch) {
368
+ resources.push({ type: "JS", path: srcMatch[1] });
369
+ }
370
+ });
371
+ }
372
+ // Images
373
+ const imgMatches = htmlContent.match(/<img[^>]*src="([^"]*)"[^>]*>/gi);
374
+ if (imgMatches) {
375
+ imgMatches.forEach((match) => {
376
+ const srcMatch = match.match(/src="([^"]*)"/i);
377
+ if (srcMatch) {
378
+ resources.push({ type: "IMG", path: srcMatch[1] });
379
+ }
380
+ });
381
+ }
382
+ if (resources.length > 0) {
383
+ console.log(`🔍 Recursos encontrados em ${htmlPath}:`);
384
+ resources.forEach((resource) => {
385
+ console.log(` ${resource.type === "CSS"
386
+ ? "🎨"
387
+ : resource.type === "JS"
388
+ ? "📜"
389
+ : "🖼️"} ${resource.path}`);
390
+ });
391
+ }
392
+ }
393
+ async serveDirectoryListing(params) {
394
+ const { dirPath, requestPath, res } = params;
395
+ // Normaliza paths para links
396
+ const basePath = requestPath.endsWith("/")
397
+ ? requestPath
398
+ : `${requestPath}/`;
399
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
400
+ const items = entries
401
+ .filter((e) => e.name !== ".DS_Store")
402
+ .sort((a, b) => {
403
+ // Pastas primeiro
404
+ if (a.isDirectory() && !b.isDirectory())
405
+ return -1;
406
+ if (!a.isDirectory() && b.isDirectory())
407
+ return 1;
408
+ return a.name.localeCompare(b.name);
409
+ })
410
+ .map((e) => {
411
+ const isDir = e.isDirectory();
412
+ const name = e.name;
413
+ const href = `${basePath}${encodeURIComponent(name)}${isDir ? "/" : ""}`;
414
+ const icon = isDir ? "📁" : this.getFileIconByName(name);
415
+ const type = isDir
416
+ ? "Pasta"
417
+ : path.extname(name).slice(1).toUpperCase() || "FILE";
418
+ return { name, href, icon, type, isDir };
419
+ });
420
+ const parentHref = basePath !== "/" ? "../" : null;
421
+ const html = `<!doctype html>
422
+ <html lang="pt-BR">
423
+ <head>
424
+ <meta charset="utf-8" />
425
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
426
+ <title>Index of ${this.escapeHtml(requestPath)}</title>
427
+ <style>
428
+ :root {
429
+ color-scheme: dark;
430
+ --bg: #000;
431
+ --card: #0b0b0b;
432
+ --border: rgba(255,255,255,.10);
433
+ --text: rgba(255,255,255,.92);
434
+ --muted: rgba(255,255,255,.62);
435
+ --hover: rgba(255,255,255,.06);
436
+ --shadow: 0 10px 30px rgba(0,0,0,.45);
437
+ }
438
+ * { box-sizing: border-box; }
439
+ body {
440
+ margin: 0;
441
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
442
+ background: var(--bg);
443
+ color: var(--text);
444
+ }
445
+ .wrap {
446
+ max-width: 1100px;
447
+ margin: 0 auto;
448
+ padding: 20px;
449
+ }
450
+ header {
451
+ display: flex;
452
+ gap: 12px;
453
+ align-items: baseline;
454
+ justify-content: space-between;
455
+ margin-bottom: 16px;
456
+ }
457
+ h1 {
458
+ margin: 0;
459
+ font-size: 18px;
460
+ font-weight: 650;
461
+ letter-spacing: .2px;
462
+ }
463
+ .path {
464
+ color: var(--muted);
465
+ font-size: 13px;
466
+ overflow: hidden;
467
+ text-overflow: ellipsis;
468
+ white-space: nowrap;
469
+ max-width: 60vw;
470
+ }
471
+ .grid {
472
+ display: grid;
473
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
474
+ gap: 12px;
475
+ }
476
+ a.card {
477
+ display: flex;
478
+ gap: 12px;
479
+ align-items: center;
480
+ text-decoration: none;
481
+ color: inherit;
482
+ padding: 14px 14px;
483
+ border: 1px solid var(--border);
484
+ border-radius: 12px;
485
+ background: var(--card);
486
+ box-shadow: var(--shadow);
487
+ transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
488
+ will-change: transform;
489
+ }
490
+ a.card:hover {
491
+ transform: translateY(-2px);
492
+ border-color: rgba(255,255,255,.22);
493
+ background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
494
+ }
495
+ .icon {
496
+ width: 38px;
497
+ height: 38px;
498
+ display: grid;
499
+ place-items: center;
500
+ border-radius: 10px;
501
+ border: 1px solid var(--border);
502
+ background: rgba(255,255,255,.03);
503
+ flex: 0 0 auto;
504
+ font-size: 18px;
505
+ }
506
+ .meta { min-width: 0; }
507
+ .name {
508
+ font-size: 14px;
509
+ font-weight: 600;
510
+ line-height: 1.25;
511
+ overflow: hidden;
512
+ text-overflow: ellipsis;
513
+ white-space: nowrap;
514
+ max-width: 100%;
515
+ }
516
+ .type {
517
+ margin-top: 3px;
518
+ font-size: 12px;
519
+ color: var(--muted);
520
+ }
521
+ .back {
522
+ margin: 14px 0 0;
523
+ font-size: 13px;
524
+ color: var(--muted);
525
+ }
526
+ .back a { color: var(--text); text-decoration: none; border-bottom: 1px dotted rgba(255,255,255,.25); }
527
+ </style>
528
+ </head>
529
+ <body>
530
+ <div class="wrap">
531
+ <header>
532
+ <h1>Directory listing</h1>
533
+ <div class="path">${this.escapeHtml(requestPath)}</div>
534
+ </header>
535
+
536
+ <div class="grid">
537
+ ${parentHref
538
+ ? `<a class="card" href="${parentHref}"><div class="icon">↩</div><div class="meta"><div class="name">..</div><div class="type">Voltar</div></div></a>`
539
+ : ""}
540
+ ${items
541
+ .map((i) => `
542
+ <a class="card" href="${i.href}">
543
+ <div class="icon">${i.icon}</div>
544
+ <div class="meta">
545
+ <div class="name">${this.escapeHtml(i.name)}${i.isDir ? "/" : ""}</div>
546
+ <div class="type">${this.escapeHtml(i.type)}</div>
547
+ </div>
548
+ </a>
549
+ `.trim())
550
+ .join("")}
551
+ </div>
552
+
553
+ <div class="back">
554
+ Servido por <strong>purecore-hot-server</strong>
555
+ </div>
556
+ </div>
557
+ </body>
558
+ </html>`;
559
+ res.writeHead(200, {
560
+ "Content-Type": "text/html; charset=utf-8",
561
+ "Access-Control-Allow-Origin": "*",
562
+ "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
563
+ "Access-Control-Allow-Headers": "Content-Type",
564
+ });
565
+ res.end(html);
566
+ console.log(`📁 Directory listing: ${path.relative(this.config.root, dirPath)}`);
567
+ }
568
+ getFileIconByName(fileName) {
569
+ const ext = path.extname(fileName).toLowerCase();
570
+ if (ext === ".html" || ext === ".htm")
571
+ return "🌐";
572
+ if (ext === ".css")
573
+ return "🎨";
574
+ if (ext === ".js" || ext === ".mjs" || ext === ".ts")
575
+ return "📜";
576
+ if (ext === ".json" || ext === ".yaml" || ext === ".yml" || ext === ".toml")
577
+ return "🧩";
578
+ if (ext === ".md" || ext === ".txt")
579
+ return "📝";
580
+ if ([
581
+ ".png",
582
+ ".jpg",
583
+ ".jpeg",
584
+ ".gif",
585
+ ".svg",
586
+ ".webp",
587
+ ".bmp",
588
+ ".tif",
589
+ ".tiff",
590
+ ".ico",
591
+ ].includes(ext))
592
+ return "🖼️";
593
+ if ([
594
+ ".mp4",
595
+ ".webm",
596
+ ".mov",
597
+ ".avi",
598
+ ".wmv",
599
+ ".flv",
600
+ ".mkv",
601
+ ".ogg",
602
+ ].includes(ext))
603
+ return "🎞️";
604
+ if ([".mp3", ".wav", ".m4a", ".aac", ".opus", ".oga"].includes(ext))
605
+ return "🎵";
606
+ if ([".zip", ".tar", ".gz", ".gzip"].includes(ext))
607
+ return "🗜️";
608
+ if (ext === ".pdf")
609
+ return "📄";
610
+ return "📄";
611
+ }
612
+ escapeHtml(input) {
613
+ return input
614
+ .replaceAll("&", "&amp;")
615
+ .replaceAll("<", "&lt;")
616
+ .replaceAll(">", "&gt;")
617
+ .replaceAll('"', "&quot;")
618
+ .replaceAll("'", "&#039;");
619
+ }
620
+ getNetworkIPs() {
621
+ const interfaces = os.networkInterfaces();
622
+ const ips = [];
623
+ for (const name of Object.keys(interfaces)) {
624
+ for (const iface of interfaces[name] || []) {
625
+ if (iface.family === "IPv4" && !iface.internal) {
626
+ ips.push(iface.address);
627
+ }
628
+ }
629
+ }
630
+ return ips;
631
+ }
632
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // File: test/server.test.ts
4
+ const validator_1 = require("../validator");
5
+ describe('Auto-Server Internals', () => {
6
+ test('Validator: should parse valid config', () => {
7
+ const input = {
8
+ port: 3000,
9
+ root: '/usr/www',
10
+ open: 'true'
11
+ };
12
+ const output = validator_1.configSchema.parse(input);
13
+ expect(output).toEqual(input);
14
+ });
15
+ test('Validator: should fail on invalid types', () => {
16
+ const input = {
17
+ port: "3000", // should be number
18
+ root: '/usr/www',
19
+ open: 'true'
20
+ };
21
+ expect(() => validator_1.configSchema.parse(input)).toThrow();
22
+ });
23
+ test('Mini-Zod: should handle nested objects', () => {
24
+ const schema = validator_1.z.object({
25
+ user: validator_1.z.object({
26
+ name: validator_1.z.string()
27
+ })
28
+ });
29
+ const data = { user: { name: 'Dev' } };
30
+ expect(schema.parse(data)).toEqual(data);
31
+ });
32
+ // Nota: Testes de integração (subir servidor real) em ambiente "zero deps"
33
+ // costumam ser feitos mockando o módulo 'http', mas foge ao escopo simples aqui.
34
+ });