@randomdev/pulsedev 0.1.2

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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * no podemos usar el metodo write porque apendea, y aqui necesitamos reescribir
3
+ * y tampoco write por estariamos creando una referencia circular con el writter que ejecuta counter
4
+ */
5
+ import { writeFile, readFile } from "node:fs/promises";
6
+ /**
7
+ * vamos a crar un hilo de procesador que se ocupe especificamente de la tarea de leer, cortar y unir
8
+ */
9
+ import { parentPort } from "node:worker_threads";
10
+
11
+ parentPort.on("message", async (data) => {
12
+ const { path, logLimit } = data;
13
+
14
+ try {
15
+ let text = await readFile(path, "utf-8");
16
+ if (!text) return;
17
+
18
+ const MAX_CHARACTERS = (logLimit || 1) * 1000;
19
+
20
+ if (text.length > MAX_CHARACTERS) {
21
+ // 1. convertimos en array por salto de pagina
22
+ let lines = text.split("\n");
23
+
24
+ while (lines.join("\n").length > (MAX_CHARACTERS * 0.8)) {
25
+ // 1.1 sistema FIFO(first in, first out)
26
+ // los logs mas viejos son los primeros en entrar, y por ende los primeros en borrarse
27
+ lines.shift();
28
+ }
29
+
30
+ // 2. volvemos a unir
31
+ let cleanedText = lines.join("\n");
32
+
33
+ await writeFile(path, cleanedText, "utf-8");
34
+ }
35
+ } catch (error) {
36
+ console.error("[worker] - error: " + error.message);
37
+ } finally {
38
+ process.exit(0);
39
+ }
40
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Debemos comparar el hash existe en un archivo con el nuevo creado
3
+ */
4
+
5
+ import { createHash } from "node:crypto";
6
+ import { readFile } from "node:fs/promises";
7
+
8
+ const fileHashes = new Map();
9
+
10
+ export async function compareChanges(filepath) {
11
+ try {
12
+ const content = await readFile(filepath, "utf8");
13
+
14
+ const currentHash = createHash("md5").update(content).digest("hex");
15
+ // ver si el mismo archivo ya existe en map
16
+ const previousHash = fileHashes.get(filepath);
17
+
18
+ // si son iguales se retorna false
19
+ if(currentHash === previousHash) {
20
+ return false;
21
+ }
22
+
23
+ // si no lo son guardamos el hash en el map y retornamos verdadero
24
+ fileHashes.set(filepath, currentHash);
25
+ return true
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * inyectamos el hash a la url del archivo
33
+ */
34
+
35
+ export function injectToUrl(urlPath, hash){
36
+ if(!hash) return urlPath;
37
+
38
+ return `${urlPath}?v=${hash.substring(0,8)}`;
39
+ }
@@ -0,0 +1,75 @@
1
+ export const contentTypes = {
2
+ 'html': 'text/html',
3
+ 'htm': 'text/html',
4
+ 'css': 'text/css',
5
+ 'js': 'application/javascript',
6
+ 'mjs': 'application/javascript',
7
+ 'json': 'application/json',
8
+ 'xml': 'application/xml',
9
+ 'txt': 'text/plain',
10
+ 'md': 'text/markdown',
11
+
12
+ 'svg': 'image/svg+xml',
13
+ 'png': 'image/png',
14
+ 'jpg': 'image/jpeg',
15
+ 'jpeg': 'image/jpeg',
16
+ 'gif': 'image/gif',
17
+ 'webp': 'image/webp',
18
+ 'avif': 'image/avif',
19
+ 'ico': 'image/x-icon',
20
+ 'bmp': 'image/bmp',
21
+ 'tiff': 'image/tiff',
22
+ 'tif': 'image/tiff',
23
+
24
+ 'ttf': 'font/ttf',
25
+ 'otf': 'font/otf',
26
+ 'woff': 'font/woff',
27
+ 'woff2': 'font/woff2',
28
+ 'eot': 'application/vnd.ms-fontobject',
29
+
30
+ 'mp3': 'audio/mpeg',
31
+ 'wav': 'audio/wav',
32
+ 'ogg': 'audio/ogg',
33
+ 'm4a': 'audio/mp4',
34
+ 'flac': 'audio/flac',
35
+ 'aac': 'audio/aac',
36
+ 'opus': 'audio/opus',
37
+
38
+ 'mp4': 'video/mp4',
39
+ 'webm': 'video/webm',
40
+ 'ogv': 'video/ogg',
41
+ 'avi': 'video/x-msvideo',
42
+ 'mov': 'video/quicktime',
43
+ 'mkv': 'video/x-matroska',
44
+ 'flv': 'video/x-flv',
45
+ 'mpeg': 'video/mpeg',
46
+ 'mpg': 'video/mpeg',
47
+
48
+ 'pdf': 'application/pdf',
49
+ 'doc': 'application/msword',
50
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
51
+ 'xls': 'application/vnd.ms-excel',
52
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
53
+ 'ppt': 'application/vnd.ms-powerpoint',
54
+ 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
55
+ 'zip': 'application/zip',
56
+ 'rar': 'application/x-rar-compressed',
57
+ '7z': 'application/x-7z-compressed'
58
+ };
59
+
60
+ export const binaryExtensions = new Set([
61
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico', 'bmp', 'tiff', 'tif',
62
+ 'ttf', 'otf', 'woff', 'woff2', 'eot',
63
+ 'mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'opus',
64
+ 'mp4', 'webm', 'ogv', 'avi', 'mov', 'mkv', 'flv', 'mpeg', 'mpg',
65
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', '7z'
66
+ ]);
67
+
68
+ export const noHashExtensions = new Set([
69
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico', 'bmp', 'tiff', 'tif',
70
+ 'ttf', 'otf', 'woff', 'woff2', 'eot',
71
+ 'mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'opus',
72
+ 'mp4', 'webm', 'ogv', 'avi', 'mov', 'mkv', 'flv', 'mpeg', 'mpg',
73
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', '7z',
74
+ 'svg'
75
+ ]);
@@ -0,0 +1,33 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { injectToUrl } from "./hashFingerprint.js";
6
+
7
+ async function replaceAsync(str, regex, asyncFn) {
8
+ const promises = [];
9
+ str.replace(regex, (match, ...args) => {
10
+ promises.push(asyncFn(match, ...args));
11
+ return match;
12
+ });
13
+ const data = await Promise.all(promises);
14
+ return str.replace(regex, () => data.shift());
15
+ }
16
+
17
+ export async function injectResourceHashes(htmlContent, currentDirectory) {
18
+ const regexResources = /(src|href)="([^"]+\.(js|css))"/g;
19
+
20
+ const result = await replaceAsync(htmlContent, regexResources, async (match, attr, routeResource) => {
21
+ const pathResource = join(currentDirectory, "src", routeResource);
22
+
23
+ if (existsSync(pathResource)) {
24
+ const resourceContent = await readFile(pathResource, "utf8");
25
+ const resourceHash = createHash("md5").update(resourceContent).digest("hex");
26
+ const urlConHash = injectToUrl(routeResource, resourceHash);
27
+ return `${attr}="${urlConHash}"`;
28
+ }
29
+ return match;
30
+ });
31
+
32
+ return result;
33
+ }
@@ -0,0 +1,158 @@
1
+ import { watch, stat } from "node:fs/promises";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { join, extname, basename } from "node:path";
4
+ import { hostname } from "node:os";
5
+ import { styleText } from "node:util";
6
+ import { compareChanges } from "./hashFingerprint.js";
7
+ import { write } from "./writter.js";
8
+
9
+ // Almacena referencias a watchers activos para poder cerrarlos desde tests
10
+ const _activeWatchers = [];
11
+
12
+ /**
13
+ * Cierra todos los watchers activos. Exportado para testing.
14
+ */
15
+ export async function _closeActiveWatchers() {
16
+ const closers = [..._activeWatchers];
17
+ _activeWatchers.length = 0;
18
+ await Promise.allSettled(closers.map(c => { try { return c(); } catch {} }));
19
+ }
20
+
21
+ export const startWatcher = async (config, currentDirectory, reloadServer, abortSignal) => {
22
+ if (!Array.isArray(config.watchPath)) {
23
+ console.log(styleText("bgRed", " error ") +
24
+ ' watchPath debe ser un array en pulsedev.json. Ejemplo: ["*"] o ["./src/css", "./src/js"]');
25
+ process.exit(1);
26
+ }
27
+
28
+ const extensionesIgnoradas = (Array.isArray(config.ignoreExtensions)
29
+ ? config.ignoreExtensions
30
+ : []
31
+ ).map(ext => ext.replace("*", ""));
32
+
33
+ const isWildcard = config.watchPath.includes("*");
34
+
35
+ /**
36
+ * VALIDACIÓN DE CONFIGURACIÓN DE ESCUCHA (RECURSIVE)
37
+ *
38
+ * Determinamos si el watcher debe ser recursivo basándonos en la configuración
39
+ * y el tipo de ruta (comodín o rutas específicas).
40
+ */
41
+ let isRecursive = config.recursive ?? true;
42
+
43
+ /**
44
+ * Caso 1: watchPath es wildcard ["*"] y recursive es false
45
+ * → Configuración inválida: el watcher solo escucharía la raíz (pulsedev.json).
46
+ * Forzamos recursive: true para que detecte cambios en /src y subcarpetas.
47
+ */
48
+ if (isWildcard && config.recursive === false) {
49
+ console.log(
50
+ styleText("bgYellow", " advertencia ") +
51
+ " recursive: false no tiene efecto cuando watchPath es [\"*\"]." +
52
+ " El watcher solo escucharía la raíz del proyecto y nunca detectaría" +
53
+ " cambios en src/. Se usará recursive: true automáticamente." +
54
+ " Para usar recursive: false definí rutas específicas en watchPath," +
55
+ " por ejemplo: [\"./src/css\", \"./src/js\"]."
56
+ );
57
+ isRecursive = true;
58
+ }
59
+
60
+ /**
61
+ * Caso 2: watchPath tiene rutas propias y recursive es false
62
+ * → Configuración válida: el usuario elige vigilar solo la raíz de carpetas específicas.
63
+ * Informamos al usuario que el modo no recursivo está activo.
64
+ */
65
+ if (!isWildcard && config.recursive === false) {
66
+ console.log(
67
+ styleText("bgCyan", " info ") +
68
+ " Modo de escucha no recursivo activo." +
69
+ " Solo se vigilarán los archivos en la raíz de cada ruta configurada en watchPath." +
70
+ " Los subdirectorios no serán vigilados."
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Definimos las rutas absolutas a vigilar.
76
+ * Si es wildcard, vigilamos el directorio actual (raíz).
77
+ */
78
+ const pathToWatch = isWildcard
79
+ ? [currentDirectory]
80
+ : config.watchPath.map(p => join(currentDirectory, p));
81
+
82
+ // debounce: esperamos el tiempo configurado antes de ejecutar el reload
83
+ // si llegan multiples eventos seguidos, solo se ejecuta una vez al final
84
+ let debounceTimer = null;
85
+ const debounceDelay = (config.debounceDelay || 0.5) * 1000;
86
+
87
+ const triggerReload = () => {
88
+ clearTimeout(debounceTimer);
89
+ debounceTimer = setTimeout(() => {
90
+ reloadServer();
91
+ }, debounceDelay);
92
+ };
93
+
94
+ for (const target of pathToWatch) {
95
+ // IIFE correctamente invocada con () al final
96
+ (async () => {
97
+ try {
98
+ // Iniciamos el watcher con el flag de recursividad validado previamente
99
+ // y la señal de aborto para poder cerrarlo desde tests sin dejar el event loop colgado
100
+ const watchOptions = { recursive: isRecursive, persistent: false };
101
+ if (abortSignal) watchOptions.signal = abortSignal;
102
+ const watcher = watch(target, watchOptions);
103
+
104
+ // Guardamos referencia para poder cerrarlo desde tests
105
+ _activeWatchers.push(async () => { try { await watcher.close(); } catch {} });
106
+
107
+ // Generador Asíncrono, "escupe" promesas en cada iteración del bucle
108
+ for await (const event of watcher) {
109
+ const { filename } = event;
110
+ if (!filename) continue;
111
+
112
+ let absolutePath = join(target, filename);
113
+
114
+ /**
115
+ * si la ruta que formamos es un carpeta el fingerprint intentara leerla y fallará
116
+ * debemos evitar que intente hacerlo, solo se puede leer archivos
117
+ */
118
+ try {
119
+ const stats = await stat(absolutePath);
120
+ if (stats.isDirectory()) continue;
121
+ } catch (error) {
122
+ continue;
123
+ }
124
+
125
+ let ext = extname(absolutePath);
126
+ let name = basename(absolutePath);
127
+
128
+ // ignorar extensiones configuradas
129
+ if (extensionesIgnoradas.includes(ext)) continue;
130
+
131
+ // ignorar archivos de log propios y archivos ocultos
132
+ if (name === "requests.log" || name === "errors.log" || name.startsWith(".")) continue;
133
+
134
+ // solo recargar si el contenido realmente cambio
135
+ let hasChange = await compareChanges(absolutePath);
136
+
137
+ if (hasChange) {
138
+ console.log(styleText("bgCyan", " watcher ") + ` archivo modificado: ${filename}`);
139
+ triggerReload();
140
+ }
141
+ }
142
+
143
+ } catch (err) {
144
+ if (err.code === 'ENOENT' || err.name === 'AbortError') return;
145
+
146
+ if (config.outputPath === "log") {
147
+ if (!existsSync(`${currentDirectory}/logs`)) {
148
+ mkdirSync(join(currentDirectory, 'logs'), { recursive: true });
149
+ }
150
+ let messageError = `${Date.now()} - ${hostname()}/${currentDirectory}: ${err.message}`;
151
+ await write(`${currentDirectory}/logs/errors.log`, messageError);
152
+ } else {
153
+ console.error(`Error en el watcher sobre la ruta ${target}:`, err.message);
154
+ }
155
+ }
156
+ })(); // <- () que faltaba
157
+ }
158
+ };
@@ -0,0 +1,82 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const clients = new Set();
4
+
5
+ export function initWebSocket(httpServer) {
6
+
7
+ /**
8
+ * - "upgrade" es el evento que permite cambiar el modo de comunicación del servidor
9
+ */
10
+
11
+ httpServer.on("upgrade", (req, socket) => {
12
+ // verificar que el cliente haya enviado una key
13
+ const key = req.headers['sec-websocket-key'];
14
+ if (!key) {
15
+ socket.destroy();
16
+ return;
17
+ }
18
+
19
+ /* hasheamos la key, le damos un secret(GUID - identificador unico global) reconocible para cualquier navegador y la escribimos en el socket
20
+ * - el digest convierte el binario en ASCII
21
+ * - se escribe en socket para secuestrar la conexion tcp, y convertilo en un canal bidireccional
22
+ */
23
+ const acceptKey = crypto
24
+ .createHash("sha1")
25
+ .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
26
+ .digest('base64')
27
+
28
+ socket.write(
29
+ [
30
+ "HTTP/1.1 101 Switching Protocols",
31
+ "Upgrade: websocket",
32
+ "Connection: Upgrade",
33
+ `Sec-Websocket-Accept: ${acceptKey}`,
34
+ "\r\n"
35
+ ].join("\r\n")
36
+ );
37
+
38
+ /**
39
+ * agregar el cliente al set
40
+ */
41
+ clients.add(socket);
42
+
43
+ socket.on("close", () => clients.delete(socket));
44
+ socket.on("error", () => clients.delete(socket));
45
+ });
46
+ }
47
+
48
+ export function closeAllConnections() {
49
+ /**
50
+ * Frame WebSocket de texto con payload "reload" (6 bytes).
51
+ * Construido manualmente:
52
+ * - byte 1: 0x81 (FIN=1, opcode=1 = texto)
53
+ * - byte 2: longitud del payload (sin máscara, <126)
54
+ * - resto: payload
55
+ * El cliente lo lee en onmessage y dispara location.reload().
56
+ */
57
+ const payload = "reload";
58
+ const payloadBuffer = Buffer.from(payload, "utf-8");
59
+ const reloadFrame = Buffer.concat([
60
+ Buffer.from([0x81, payloadBuffer.length]),
61
+ payloadBuffer
62
+ ]);
63
+
64
+ for (const c of clients) {
65
+ try {
66
+ c.write(reloadFrame);
67
+ } catch (e) {
68
+ // el socket puede estar ya cerrado o en estado inválido: lo destruimos igual
69
+ }
70
+ try {
71
+ c.destroy();
72
+ } catch (e) {
73
+ console.error("WebSocket: error al cerrar conexión:", e.message);
74
+ }
75
+ }
76
+ /**
77
+ * Limpiar el Set como garantía extra. Si un destroy() falla silenciosamente
78
+ * (catch vacío), el socket muerto quedaría en clients indefinidamente.
79
+ * clients.clear() asegura que el Set queda vacío sin importar qué.
80
+ */
81
+ clients.clear();
82
+ }
@@ -0,0 +1,85 @@
1
+ import { styleText } from "node:util";
2
+ import { readFile, appendFile, mkdir, writeFile as fsWriteFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { Worker } from "node:worker_threads";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ let pendingLogCheck = null;
10
+
11
+ export const write = async (pathFile, text, logLimit = 5) => {
12
+ if (!pathFile || !text) {
13
+ console.log(styleText("bgCyan", " advertencia: ") + "No se ha especificado ruta.");
14
+ return;
15
+ }
16
+
17
+ try {
18
+ const folder = dirname(pathFile);
19
+
20
+ await mkdir(folder, { recursive: true });
21
+
22
+ await appendFile(pathFile, text + "\n", "utf-8");
23
+
24
+ if (pathFile.endsWith("requests.log")) {
25
+ clearTimeout(pendingLogCheck);
26
+ pendingLogCheck = setTimeout(() => {
27
+ const workerPath = join(__dirname, "counterChar.js");
28
+ const worker = new Worker(workerPath);
29
+ worker.unref();
30
+
31
+ worker.postMessage({
32
+ path: pathFile,
33
+ logLimit: logLimit
34
+ });
35
+
36
+ worker.on("error", (err) => {
37
+ console.error("Error de ejecución del worker de logs: ", err.message);
38
+ });
39
+ }, 1000);
40
+ pendingLogCheck.unref();
41
+ }
42
+
43
+ } catch (err) {
44
+ console.error(styleText("bgRed", " error crítico de escritura: ") + err.message);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Sobrescribe un archivo con el contenido dado (NO append).
50
+ * A diferencia de write():
51
+ * - No agrega "\n" al final (escribe el contenido tal cual)
52
+ * - No dispara el worker de logs
53
+ * - Crea la carpeta padre si no existe
54
+ * Pensado para archivos de configuración completos (JSON, etc) que
55
+ * se reescriben enteros.
56
+ */
57
+ export const writeFile = async (pathFile, content) => {
58
+ if (!pathFile || content === undefined || content === null) {
59
+ console.log(styleText("bgCyan", " advertencia: ") + "No se ha especificado ruta o contenido.");
60
+ return;
61
+ }
62
+
63
+ try {
64
+ const folder = dirname(pathFile);
65
+ await mkdir(folder, { recursive: true });
66
+ await fsWriteFile(pathFile, content, "utf-8");
67
+ } catch (err) {
68
+ console.error(styleText("bgRed", " error crítico de escritura: ") + err.message);
69
+ }
70
+ }
71
+
72
+ export const read = async (pathFile) => {
73
+ if (!pathFile) {
74
+ console.log(styleText("bgCyan", " advertencia: ") + "No se ha especificado ruta.");
75
+ return null;
76
+ }
77
+
78
+ try {
79
+ const file = await readFile(pathFile, "utf-8");
80
+ return file;
81
+ } catch (err) {
82
+ // Si el archivo no existe al intentar leerlo, devolvemos null pacíficamente
83
+ return null;
84
+ }
85
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@randomdev/pulsedev",
3
+ "version": "0.1.2",
4
+ "engines": {
5
+ "node": ">=22.0.0"
6
+ },
7
+ "description": "⚡ PulseDev - Fast, native Node.js development server featuring live reload, recursive file watching, configurable debouncing, native socket for client, and comprehensive logging without production dependencies",
8
+ "bin": {
9
+ "pulsedev": "./bin/cli.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "core/",
14
+ "helpers/",
15
+ "web/"
16
+ ],
17
+ "keywords": ["guard","hot-reload", "reload","server","live-server", "watcher", "dev-server", "nodejs", "frontend", "socket", "node socket"],
18
+ "author": "randomdev",
19
+ "license": "ISC",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/pabloacisera/pulsedev_linux.git"
23
+ },
24
+ "homepage": "https://github.com/pabloacisera/pulsedev_linux#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/pabloacisera/pulsedev_linux/issues"
27
+ },
28
+ "type": "module",
29
+ "scripts": {
30
+ "test": "node --test tests/*.test.js",
31
+ "test:watch": "node --test --watch tests/*.test.js",
32
+ "test:hash": "node --test tests/hashFingerprint.test.js",
33
+ "test:writter": "node --test tests/writter.test.js",
34
+ "test:counter": "node --test tests/counterChar.test.js",
35
+ "test:server": "node --test tests/server.test.js"
36
+ }
37
+ }
Binary file
@@ -0,0 +1 @@
1
+ <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>npm</title><path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"/></svg>
@@ -0,0 +1,65 @@
1
+ <div class="docs-content">
2
+ <div class="docs-header">
3
+ <h1>⚡ PulseDev Documentation</h1>
4
+ <p class="docs-subtitle">Servidor local nativo · Cero dependencias · Hot Reload</p>
5
+ </div>
6
+
7
+ <h2>🚀 Comenzar</h2>
8
+ <p>Edita los archivos dentro del directorio <code>src/</code> de tu proyecto. PulseDev detectará automáticamente cualquier cambio en el sistema de archivos y actualizará el navegador al instante.</p>
9
+
10
+ <pre><code>src/
11
+ ├── index.html ← Punto de entrada principal
12
+ ├── css/
13
+ │ └── index.css ← Estilos de la aplicación
14
+ ├── js/
15
+ │ └── index.js ← Lógica del lado del cliente
16
+ └── assets/ ← Recursos estáticos (imágenes, fuentes)</code></pre>
17
+
18
+ <h2>⚙️ Configuración — <code>pulsedev.json</code></h2>
19
+ <div class="table-container">
20
+ <table>
21
+ <thead>
22
+ <tr>
23
+ <th>Propiedad</th>
24
+ <th>Descripción</th>
25
+ <th>Por defecto</th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <tr><td><code>watchPath</code></td><td>Directorios bajo vigilancia del watcher</td><td><code>["*"]</code></td></tr>
30
+ <tr><td><code>runFile</code></td><td>Archivo HTML de entrada inicial</td><td><code>"index.html"</code></td></tr>
31
+ <tr><td><code>port</code></td><td>Puerto de escucha del servidor local</td><td><code>3003</code></td></tr>
32
+ <tr><td><code>outputPath</code></td><td>Destino de logs: archivo (<code>"log"</code>) o salida estándar (<code>"terminal"</code>)</td><td><code>"log"</code></td></tr>
33
+ <tr><td><code>ignoreExtensions</code></td><td>Filtros de exclusión para el Hot Reload</td><td><code>["*.txt","*.log","*.env","*.md"]</code></td></tr>
34
+ <tr><td><code>debounceDelay</code></td><td>Tiempo de espera (segundos) antes de refrescar</td><td><code>0.5</code></td></tr>
35
+ <tr><td><code>recursive</code></td><td>Vigilancia de subdirectorios del árbol</td><td><code>true</code></td></tr>
36
+ <tr><td><code>logLimit</code></td><td>Umbral máximo del archivo de logs (KB)</td><td><code>5</code></td></tr>
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+
41
+ <h2>🖥️ Comandos disponibles</h2>
42
+ <pre><code>pulsedev init # Inicializa la estructura base del proyecto
43
+ pulsedev run # Ejecuta el servidor de desarrollo y el watcher
44
+ pulsedev --help # Muestra el panel de ayuda y opciones
45
+ pulsedev --version # Devuelve la versión instalada en el sistema</code></pre>
46
+
47
+ <h2>👁️ Hot Reload Automático</h2>
48
+ <p>PulseDev monitorea el estado del proyecto utilizando primitivas nativas de <code>fs.watch</code>. Al detectar una modificación, realiza una verificación mediante un hash <strong>MD5</strong> para asegurar un cambio real en el contenido, evitando falsos positivos. Las respuestas de recursos estáticos incluyen políticas dinámicas de <em>cache busting</em> para forzar la lectura del nuevo código.</p>
49
+
50
+ <h3>🔌 Infraestructura WebSocket</h3>
51
+ <p>El mecanismo de sincronización utiliza un canal de comunicación bidireccional mediante <strong>WebSockets</strong> de bajo overhead:</p>
52
+ <ol>
53
+ <li>El cliente inicializa y sostiene una conexión persistente contra el socket del servidor de desarrollo.</li>
54
+ <li>Ante una recarga del proceso del servidor, las conexiones se finalizan de manera controlada.</li>
55
+ <li>El script cliente captura los eventos nativos de desconexión (<code>onclose</code> / <code>onerror</code>).</li>
56
+ <li>Se dispara una recarga forzada en el navegador web para reflejar el último estado.</li>
57
+ </ol>
58
+
59
+ <h3>📝 Arquitectura de Logs</h3>
60
+ <p>Por omisión, las interacciones HTTP son procesadas y enviadas hacia <code>logs/requests.log</code>. Un subproceso en segundo plano (<strong>Worker Thread</strong> nativo) gestiona las tareas de rotación del archivo en función de la propiedad <code>logLimit</code>, previniendo cuellos de botella en el bucle de eventos principal.</p>
61
+
62
+ <blockquote>
63
+ ⚡ PulseDev · Optimizado sobre Node.js Nativo · Peso total: 1.7MB
64
+ </blockquote>
65
+ </div>