@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/.hot-server-certs/localhost.crt +22 -0
- package/.hot-server-certs/localhost.crt.bak. +13 -0
- package/.hot-server-certs/localhost.key +28 -0
- package/.hot-server-certs/localhost.key.bak. +28 -0
- package/APAGAR_cria.sh +51 -0
- package/CHANGELOG.md +89 -0
- package/COMPARACAO_LIVE_SERVER.md +101 -0
- package/README.md +197 -0
- package/dist/cert-generator.js +155 -0
- package/dist/deployer.js +128 -0
- package/dist/index.js +40 -0
- package/dist/server.js +632 -0
- package/dist/test/server.test.js +34 -0
- package/dist/validator.js +49 -0
- package/dist/watcher.js +58 -0
- package/hot-server.deps.graflow +1 -0
- package/install-ignite.ps1 +26 -0
- package/install-ignite.sh +25 -0
- package/jest.config.cjs +7 -0
- package/package.json +36 -0
- package/reports/21-12-2025_04-20.md +154 -0
- package/reports/21-12-2025_04-21.md +184 -0
- package/reports/31-12-2025_21-15.md +29 -0
- package/reports/31-12-2025_21-45.md +19 -0
- package/reports/31-12-2025_22-00.md +31 -0
- package/src/cert-generator.ts +173 -0
- package/src/deployer.ts +182 -0
- package/src/index.ts +44 -0
- package/src/server.ts +741 -0
- package/src/validator.ts +51 -0
- package/src/watcher.ts +63 -0
- package/tsconfig.json +15 -0
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("&", "&")
|
|
615
|
+
.replaceAll("<", "<")
|
|
616
|
+
.replaceAll(">", ">")
|
|
617
|
+
.replaceAll('"', """)
|
|
618
|
+
.replaceAll("'", "'");
|
|
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
|
+
});
|