@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.
- package/LICENSE +15 -0
- package/README.md +585 -0
- package/bin/cli.js +75 -0
- package/core/init.js +48 -0
- package/core/run.js +341 -0
- package/core/serverManager.js +299 -0
- package/helpers/counterChar.js +40 -0
- package/helpers/hashFingerprint.js +39 -0
- package/helpers/mimeTypes.js +75 -0
- package/helpers/resourceHasher.js +33 -0
- package/helpers/watcher.js +158 -0
- package/helpers/websocket.js +82 -0
- package/helpers/writter.js +85 -0
- package/package.json +37 -0
- package/web/assets/Arimo-VariableFont_wght.ttf +0 -0
- package/web/assets/favicon.png +0 -0
- package/web/assets/github.svg +1 -0
- package/web/assets/logo.png +0 -0
- package/web/assets/npm.svg +1 -0
- package/web/templates/documentation.html +65 -0
- package/web/templates/index.css +397 -0
- package/web/templates/index.html +64 -0
- package/web/templates/index.js +45 -0
- package/web/templates/pulsedev.json +10 -0
- package/web/templates/socket-client.js +6 -0
package/core/init.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
import { write } from "../helpers/writter.js";
|
|
3
|
+
import { cpSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const TEMPLATES_DIR = join(__dirname, "../web/templates");
|
|
11
|
+
|
|
12
|
+
const templates = {
|
|
13
|
+
config: readFileSync(join(TEMPLATES_DIR, "pulsedev.json"), "utf-8"),
|
|
14
|
+
html: readFileSync(join(TEMPLATES_DIR, "index.html"), "utf-8"),
|
|
15
|
+
css: readFileSync(join(TEMPLATES_DIR, "index.css"), "utf-8"),
|
|
16
|
+
js: readFileSync(join(TEMPLATES_DIR, "index.js"), "utf-8"),
|
|
17
|
+
socket: readFileSync(join(TEMPLATES_DIR, "socket-client.js"), "utf-8"),
|
|
18
|
+
docs: readFileSync(join(TEMPLATES_DIR, "documentation.html"), "utf-8"),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function runInit(currentDirectory) {
|
|
22
|
+
console.log(styleText("bgBlue", " info ") + " ejecutando init...");
|
|
23
|
+
|
|
24
|
+
const source = join(__dirname, "../web/assets");
|
|
25
|
+
const destiny = join(currentDirectory, 'src', 'assets');
|
|
26
|
+
|
|
27
|
+
mkdirSync(join(currentDirectory, 'src'), { recursive: true });
|
|
28
|
+
mkdirSync(join(currentDirectory, 'src', 'js'), { recursive: true });
|
|
29
|
+
mkdirSync(join(currentDirectory, 'src', 'css'), { recursive: true });
|
|
30
|
+
|
|
31
|
+
cpSync(source, destiny, { recursive: true });
|
|
32
|
+
|
|
33
|
+
const finalJs = templates.js.replace('__DOCUMENTATION_HTML__', JSON.stringify(templates.docs));
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
console.log(styleText("bgMagentaBright", " execute ") + " Creando archivo de configuración.");
|
|
37
|
+
await write(join(currentDirectory, 'pulsedev.json'), templates.config);
|
|
38
|
+
|
|
39
|
+
console.log(styleText("bgMagentaBright", " execute ") + " Creando archivos basicos de servidor. ");
|
|
40
|
+
await write(join(currentDirectory, 'src', 'index.html'), templates.html);
|
|
41
|
+
await write(join(currentDirectory, 'src', 'js', 'index.js'), finalJs);
|
|
42
|
+
await write(join(currentDirectory, 'src', 'js', 'socket-client.js'), templates.socket);
|
|
43
|
+
await write(join(currentDirectory, 'src', 'css', 'index.css'), templates.css);
|
|
44
|
+
console.log(styleText("bgGreenBright", " execute ") + " Revisar archivo de configuracion y ejecutar servidor con 'pulsedev run'. ");
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(styleText("bgRed", " error: ") + "Fallo al inicializar los archivos: " + err.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
package/core/run.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/run.js — Entry point del comando `pulsedev run`
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidad: orquestar todo lo concerniente al comando `run`,
|
|
5
|
+
* separado del state machine HTTP que vive en serverManager.js.
|
|
6
|
+
*
|
|
7
|
+
* ============================================================================
|
|
8
|
+
* SISTEMA DE FLAGS
|
|
9
|
+
* ============================================================================
|
|
10
|
+
*
|
|
11
|
+
* CONVENCIÓN DE FLAGS
|
|
12
|
+
* --------------------
|
|
13
|
+
* - Formato long: --nombre o --nombre=valor
|
|
14
|
+
* - Formato con valor: --port 4000 | --port=4000
|
|
15
|
+
* - Booleanos: presencia = true
|
|
16
|
+
* - Un flag desconocido (que no sea reservado ni clave del JSON) es error fatal
|
|
17
|
+
* - El schema de flags válidos = claves de tu pulsedev.json
|
|
18
|
+
*
|
|
19
|
+
* FLAGS RESERVADOS (siempre disponibles, no requieren clave en JSON)
|
|
20
|
+
* -------------------------------------------------------------------
|
|
21
|
+
* --list-flags Muestra los flags disponibles (= claves de tu JSON)
|
|
22
|
+
* --persist Si se usan flags, persiste el override al JSON en vez
|
|
23
|
+
* de solo override en memoria (opt-in, no destructivo
|
|
24
|
+
* por defecto)
|
|
25
|
+
*
|
|
26
|
+
* EJEMPLOS DE USO
|
|
27
|
+
* ---------------
|
|
28
|
+
* pulsedev run
|
|
29
|
+
* pulsedev run --list-flags
|
|
30
|
+
* pulsedev run --port 4000 # override en memoria
|
|
31
|
+
* pulsedev run --port 4000 --persist # override + escribe JSON
|
|
32
|
+
* pulsedev run --debounceDelay 1 --port 4000
|
|
33
|
+
* pulsedev run --outputPath=terminal
|
|
34
|
+
*
|
|
35
|
+
* COERCIÓN DE TIPOS
|
|
36
|
+
* -----------------
|
|
37
|
+
* Los flags llegan como string desde la CLI. Se coercionan al tipo del valor
|
|
38
|
+
* original en el JSON:
|
|
39
|
+
* - JSON boolean → "true"/"1" = true, todo lo demás = false
|
|
40
|
+
* - JSON number → Number(value) (NaN si inválido → error)
|
|
41
|
+
* - JSON array → JSON.parse(value) (si falla, queda como string)
|
|
42
|
+
* - JSON string → string
|
|
43
|
+
* - JSON null → string
|
|
44
|
+
*
|
|
45
|
+
* PRECEDENCIA (de mayor a menor)
|
|
46
|
+
* ------------------------------
|
|
47
|
+
* 1. Flag CLI (este sistema)
|
|
48
|
+
* 2. pulsedev.json (base)
|
|
49
|
+
* 3. Defaults internos de serverManager
|
|
50
|
+
*
|
|
51
|
+
* ============================================================================
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { existsSync } from "node:fs";
|
|
55
|
+
import { join } from "node:path";
|
|
56
|
+
import { hostname } from "node:os";
|
|
57
|
+
import { styleText } from "node:util";
|
|
58
|
+
|
|
59
|
+
import { read, writeFile } from "../helpers/writter.js";
|
|
60
|
+
import {
|
|
61
|
+
startServer as _startServer,
|
|
62
|
+
reloadServer as _reloadServer
|
|
63
|
+
} from "./serverManager.js";
|
|
64
|
+
|
|
65
|
+
const RESERVED_FLAGS = new Set(["list-flags", "persist"]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parsea argumentos tipo process.argv en un objeto { flag: valor }.
|
|
69
|
+
* Soporta: --key value, --key=value, --boolean (presencia = true).
|
|
70
|
+
* Args que no empiezan con -- se ignoran.
|
|
71
|
+
*/
|
|
72
|
+
function parseFlags(args) {
|
|
73
|
+
const flags = {};
|
|
74
|
+
const list = Array.isArray(args) ? args : [];
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < list.length; i++) {
|
|
77
|
+
const arg = list[i];
|
|
78
|
+
if (typeof arg !== "string" || !arg.startsWith("--")) continue;
|
|
79
|
+
|
|
80
|
+
const eqIdx = arg.indexOf("=");
|
|
81
|
+
if (eqIdx !== -1) {
|
|
82
|
+
const key = arg.slice(2, eqIdx);
|
|
83
|
+
const value = arg.slice(eqIdx + 1);
|
|
84
|
+
if (key) flags[key] = value;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const key = arg.slice(2);
|
|
89
|
+
if (!key) continue;
|
|
90
|
+
|
|
91
|
+
const next = list[i + 1];
|
|
92
|
+
if (typeof next === "string" && !next.startsWith("--")) {
|
|
93
|
+
flags[key] = next;
|
|
94
|
+
i++;
|
|
95
|
+
} else {
|
|
96
|
+
flags[key] = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return flags;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convierte un valor crudo de la CLI al tipo del valor original del JSON.
|
|
105
|
+
* Lanza Error descriptivo si la coerción es imposible.
|
|
106
|
+
*/
|
|
107
|
+
function coerceValue(rawValue, original) {
|
|
108
|
+
const isBoolFlag = rawValue === true;
|
|
109
|
+
|
|
110
|
+
if (typeof original === "boolean") {
|
|
111
|
+
if (isBoolFlag) return true;
|
|
112
|
+
const v = String(rawValue).toLowerCase();
|
|
113
|
+
if (v === "true" || v === "1" || v === "yes") return true;
|
|
114
|
+
if (v === "false" || v === "0" || v === "no") return false;
|
|
115
|
+
throw new Error(`valor booleano inválido: ${rawValue}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof original === "number") {
|
|
119
|
+
const n = Number(rawValue);
|
|
120
|
+
if (Number.isNaN(n)) throw new Error(`valor numérico inválido: ${rawValue}`);
|
|
121
|
+
return n;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(original)) {
|
|
125
|
+
if (isBoolFlag) {
|
|
126
|
+
throw new Error("se esperaba un array (ej: '[\"*\"]'), se recibió flag booleano");
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(rawValue);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error(`valor de array inválido (debe ser JSON válido): ${rawValue}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (original === null) {
|
|
136
|
+
return String(rawValue);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return String(rawValue);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Devuelve la lista de flags (no reservados) que NO existen como clave en config.
|
|
144
|
+
*/
|
|
145
|
+
function findUnknownFlags(flags, config) {
|
|
146
|
+
const configKeys = new Set(Object.keys(config));
|
|
147
|
+
return Object.keys(flags).filter(
|
|
148
|
+
(k) => !RESERVED_FLAGS.has(k) && !configKeys.has(k)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Construye un config mergeado a partir del config base y los flags parseados.
|
|
154
|
+
* No muta el config base. No toca el disco.
|
|
155
|
+
*/
|
|
156
|
+
function mergeConfig(baseConfig, flags) {
|
|
157
|
+
const merged = { ...baseConfig };
|
|
158
|
+
for (const [key, rawValue] of Object.entries(flags)) {
|
|
159
|
+
if (RESERVED_FLAGS.has(key)) continue;
|
|
160
|
+
if (!(key in merged)) continue;
|
|
161
|
+
merged[key] = coerceValue(rawValue, merged[key]);
|
|
162
|
+
}
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Lee el pulsedev.json del directorio y muestra las claves con sus valores
|
|
168
|
+
* actuales. Cada clave listada es un flag válido para `pulsedev run`.
|
|
169
|
+
*/
|
|
170
|
+
async function listFlags(currentDirectory) {
|
|
171
|
+
const configPath = join(currentDirectory, "pulsedev.json");
|
|
172
|
+
|
|
173
|
+
if (!existsSync(configPath)) {
|
|
174
|
+
console.error(
|
|
175
|
+
styleText("bgRed", " error ") +
|
|
176
|
+
" No se encontró pulsedev.json. Ejecutá `pulsedev init` primero."
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const raw = await read(configPath);
|
|
182
|
+
if (!raw) {
|
|
183
|
+
console.error(
|
|
184
|
+
styleText("bgRed", " error ") +
|
|
185
|
+
" No se pudo leer pulsedev.json (archivo vacío o ilegible)."
|
|
186
|
+
);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let config;
|
|
191
|
+
try {
|
|
192
|
+
config = JSON.parse(raw);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(
|
|
195
|
+
styleText("bgRed", " error ") +
|
|
196
|
+
` pulsedev.json tiene JSON inválido: ${err.message}`
|
|
197
|
+
);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const keys = Object.keys(config);
|
|
202
|
+
if (keys.length === 0) {
|
|
203
|
+
console.log("pulsedev.json no tiene claves configurables.");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(
|
|
208
|
+
styleText("bold", "Flags disponibles (= claves de tu pulsedev.json):")
|
|
209
|
+
);
|
|
210
|
+
const maxLen = Math.max(...keys.map((k) => k.length));
|
|
211
|
+
for (const key of keys) {
|
|
212
|
+
const current = JSON.stringify(config[key]);
|
|
213
|
+
const label = `--${key}`.padEnd(maxLen + 3);
|
|
214
|
+
console.log(` ${label} (actual: ${current})`);
|
|
215
|
+
}
|
|
216
|
+
console.log("");
|
|
217
|
+
console.log(
|
|
218
|
+
styleText("dim", " Flags reservados (no requieren clave en JSON):")
|
|
219
|
+
);
|
|
220
|
+
console.log(" --list-flags Muestra esta lista");
|
|
221
|
+
console.log(" --persist Persiste el override al JSON (opt-in)");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Carga el pulsedev.json o termina con error descriptivo.
|
|
226
|
+
*/
|
|
227
|
+
async function loadBaseConfig(currentDirectory) {
|
|
228
|
+
const configPath = join(currentDirectory, "pulsedev.json");
|
|
229
|
+
|
|
230
|
+
if (!existsSync(configPath)) {
|
|
231
|
+
console.error(
|
|
232
|
+
styleText("bgRed", " error ") +
|
|
233
|
+
" No se encontró pulsedev.json. Ejecutá `pulsedev init` primero."
|
|
234
|
+
);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const raw = await read(configPath);
|
|
239
|
+
if (!raw) {
|
|
240
|
+
console.error(
|
|
241
|
+
styleText("bgRed", " error ") +
|
|
242
|
+
" No se pudo leer pulsedev.json (archivo vacío o ilegible)."
|
|
243
|
+
);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(raw);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(
|
|
251
|
+
styleText("bgRed", " error ") +
|
|
252
|
+
` pulsedev.json tiene JSON inválido: ${err.message}`
|
|
253
|
+
);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Valida los flags contra el schema del config. Termina con error si hay
|
|
260
|
+
* flags desconocidos. Lanza Error si algún valor no se puede coercionar.
|
|
261
|
+
*/
|
|
262
|
+
function validateFlagsOrExit(flags, baseConfig) {
|
|
263
|
+
const unknown = findUnknownFlags(flags, baseConfig);
|
|
264
|
+
if (unknown.length === 0) return;
|
|
265
|
+
|
|
266
|
+
const available = Object.keys(baseConfig)
|
|
267
|
+
.map((k) => `--${k}`)
|
|
268
|
+
.join(", ");
|
|
269
|
+
console.error(
|
|
270
|
+
styleText("bgRed", " error ") +
|
|
271
|
+
` Flag(s) desconocido(s): ${unknown.map((f) => `--${f}`).join(", ")}\n` +
|
|
272
|
+
` Flags disponibles (= claves de tu pulsedev.json): ${available}\n` +
|
|
273
|
+
` Ejecutá \`pulsedev run --list-flags\` para ver el detalle de los valores actuales.`
|
|
274
|
+
);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Entry point del comando `pulsedev run`.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} currentDirectory - cwd donde está el proyecto del usuario
|
|
282
|
+
* @param {object} [options]
|
|
283
|
+
* @param {string[]} [options.argv] - args a parsear (default: process.argv.slice(3))
|
|
284
|
+
* @param {boolean} [options.silent] - suprime el banner
|
|
285
|
+
*/
|
|
286
|
+
export const runCommand = async (currentDirectory, options = {}) => {
|
|
287
|
+
const argv = Array.isArray(options.argv)
|
|
288
|
+
? options.argv
|
|
289
|
+
: process.argv.slice(3);
|
|
290
|
+
const flags = parseFlags(argv);
|
|
291
|
+
|
|
292
|
+
if (flags["list-flags"]) {
|
|
293
|
+
await listFlags(currentDirectory);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const baseConfig = await loadBaseConfig(currentDirectory);
|
|
298
|
+
validateFlagsOrExit(flags, baseConfig);
|
|
299
|
+
|
|
300
|
+
let mergedConfig = baseConfig;
|
|
301
|
+
const hasUserFlags = Object.keys(flags).some((k) => !RESERVED_FLAGS.has(k));
|
|
302
|
+
|
|
303
|
+
if (hasUserFlags) {
|
|
304
|
+
try {
|
|
305
|
+
mergedConfig = mergeConfig(baseConfig, flags);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error(
|
|
308
|
+
styleText("bgRed", " error ") +
|
|
309
|
+
` No se pudo aplicar el flag: ${err.message}`
|
|
310
|
+
);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (hasUserFlags && flags["persist"]) {
|
|
316
|
+
const configPath = join(currentDirectory, "pulsedev.json");
|
|
317
|
+
const serialized = JSON.stringify(mergedConfig, null, 2);
|
|
318
|
+
await writeFile(configPath, serialized);
|
|
319
|
+
console.log(
|
|
320
|
+
styleText("bgYellow", " persist ") +
|
|
321
|
+
" Configuración sobrescrita en pulsedev.json"
|
|
322
|
+
);
|
|
323
|
+
} else if (hasUserFlags) {
|
|
324
|
+
console.log(
|
|
325
|
+
styleText("dim", " ⓘ Flags aplicados en memoria (no se modificó el JSON). Usá --persist para persistir.")
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!options.silent) {
|
|
330
|
+
const ts = new Date().toISOString();
|
|
331
|
+
console.log(styleText("dim", `▶ PulseDev ${ts} · ${hostname()}`));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return _startServer(currentDirectory, mergedConfig);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export { _startServer as startServer, _reloadServer as reloadServer };
|
|
338
|
+
|
|
339
|
+
// exported for testing
|
|
340
|
+
export { parseFlags, coerceValue, mergeConfig, findUnknownFlags, RESERVED_FLAGS };
|
|
341
|
+
export { listFlags, loadBaseConfig, validateFlagsOrExit };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { styleText } from "node:util";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { read, write } from "../helpers/writter.js";
|
|
7
|
+
import { hostname } from "node:os";
|
|
8
|
+
import { contentTypes, binaryExtensions, noHashExtensions } from "../helpers/mimeTypes.js";
|
|
9
|
+
import { injectResourceHashes } from "../helpers/resourceHasher.js";
|
|
10
|
+
import { startWatcher, _closeActiveWatchers } from "../helpers/watcher.js";
|
|
11
|
+
import { initWebSocket, closeAllConnections } from "../helpers/websocket.js";
|
|
12
|
+
|
|
13
|
+
const state = {
|
|
14
|
+
config: null,
|
|
15
|
+
server: null,
|
|
16
|
+
isRestarting: false,
|
|
17
|
+
currentDirectory: null,
|
|
18
|
+
watcherAbortController: null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// exported for testing
|
|
22
|
+
let _reloadCount = 0;
|
|
23
|
+
|
|
24
|
+
async function upServer(config) {
|
|
25
|
+
try {
|
|
26
|
+
const server = createServer(async (req, res) => {
|
|
27
|
+
let cleanURL = req.url.split("?")[0];
|
|
28
|
+
|
|
29
|
+
let targetResource = cleanURL === '/' ? (config.runFile || 'index.html') : cleanURL;
|
|
30
|
+
let filePath = join(state.currentDirectory, 'src', targetResource);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Guard contra path traversal:
|
|
34
|
+
* Si la ruta resuelta se escapa del directorio src/ del proyecto,
|
|
35
|
+
* respondemos 403 sin tocar el disco.
|
|
36
|
+
*/
|
|
37
|
+
const srcRoot = resolve(join(state.currentDirectory, 'src'));
|
|
38
|
+
const resolvedFilePath = resolve(filePath);
|
|
39
|
+
if (!resolvedFilePath.startsWith(srcRoot)) {
|
|
40
|
+
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
41
|
+
res.end(`
|
|
42
|
+
<!DOCTYPE html>
|
|
43
|
+
<html lang="es">
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="UTF-8">
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
47
|
+
<title>403 - Acceso Denegado</title>
|
|
48
|
+
<style>
|
|
49
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
50
|
+
body {
|
|
51
|
+
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
|
|
52
|
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
53
|
+
min-height: 100vh;
|
|
54
|
+
display: flex;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
align-items: center;
|
|
57
|
+
color: #f8fafc;
|
|
58
|
+
}
|
|
59
|
+
.error-card {
|
|
60
|
+
background: #1e293b;
|
|
61
|
+
border: 1px solid #334155;
|
|
62
|
+
border-radius: 12px;
|
|
63
|
+
padding: 2.5rem;
|
|
64
|
+
text-align: center;
|
|
65
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
|
|
66
|
+
max-width: 450px;
|
|
67
|
+
width: 90%;
|
|
68
|
+
}
|
|
69
|
+
.error-code {
|
|
70
|
+
font-size: 5rem;
|
|
71
|
+
font-weight: 800;
|
|
72
|
+
background: linear-gradient(135deg, #f87171 0%, #fb923c 100%);
|
|
73
|
+
-webkit-background-clip: text;
|
|
74
|
+
background-clip: text;
|
|
75
|
+
color: transparent;
|
|
76
|
+
line-height: 1;
|
|
77
|
+
margin-bottom: 0.5rem;
|
|
78
|
+
}
|
|
79
|
+
h1 { font-size: 1.5rem; font-weight: 600; color: #f1f5f9; margin-bottom: 0.75rem; }
|
|
80
|
+
p { color: #94a3b8; font-size: 0.95rem; line-height: 1.5; margin-bottom: 1.5rem; }
|
|
81
|
+
.btn-back {
|
|
82
|
+
display: inline-block;
|
|
83
|
+
background: linear-gradient(135deg, #f87171 0%, #fb923c 100%);
|
|
84
|
+
color: #ffffff;
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
font-weight: 500;
|
|
87
|
+
font-size: 0.9rem;
|
|
88
|
+
padding: 0.75rem 1.5rem;
|
|
89
|
+
border-radius: 6px;
|
|
90
|
+
transition: transform 0.2s, opacity 0.2s;
|
|
91
|
+
}
|
|
92
|
+
.btn-back:hover { transform: translateY(-1px); opacity: 0.95; }
|
|
93
|
+
</style>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<div class="error-card">
|
|
97
|
+
<div class="error-code">403</div>
|
|
98
|
+
<h1>Acceso denegado</h1>
|
|
99
|
+
<p>La ruta solicitada está fuera del directorio permitido.</p>
|
|
100
|
+
<a href="/" class="btn-back">Volver al inicio</a>
|
|
101
|
+
</div>
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
104
|
+
`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let requestMessage = `- ${new Date().toLocaleString()} : ${req.method} : ${req.url} - ${req.headers['user-agent'] || 'unknown'}`;
|
|
109
|
+
|
|
110
|
+
if (config.outputPath && config.outputPath === "terminal") {
|
|
111
|
+
console.log(
|
|
112
|
+
styleText("dim", `- ${new Date().toLocaleString()} : `) +
|
|
113
|
+
styleText("green", req.method) +
|
|
114
|
+
styleText("dim", ` : ${req.url} - ${req.headers['user-agent'] || 'unknown'}`)
|
|
115
|
+
);
|
|
116
|
+
} else {
|
|
117
|
+
if (!existsSync(join(state.currentDirectory, 'logs'))) {
|
|
118
|
+
mkdirSync(join(state.currentDirectory, 'logs'), { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
await write(join(state.currentDirectory, 'logs', 'requests.log'), requestMessage, config.logLimit || 5);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const ext = filePath.split('.').pop();
|
|
124
|
+
|
|
125
|
+
const isBinary = binaryExtensions.has(ext);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
let content = await readFile(filePath, isBinary ? null : 'utf-8');
|
|
129
|
+
|
|
130
|
+
if ((ext === "html" || cleanURL === "/") && !noHashExtensions.has(ext)) {
|
|
131
|
+
content = await injectResourceHashes(content, state.currentDirectory);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const contentType = contentTypes[ext] || 'text/plain';
|
|
135
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
136
|
+
res.end(content);
|
|
137
|
+
} catch {
|
|
138
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
139
|
+
res.end(`
|
|
140
|
+
<!DOCTYPE html>
|
|
141
|
+
<html lang="es">
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="UTF-8">
|
|
144
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
145
|
+
<title>404 - Archivo No Encontrado</title>
|
|
146
|
+
<style>
|
|
147
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
148
|
+
body {
|
|
149
|
+
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
|
|
150
|
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
151
|
+
min-height: 100vh;
|
|
152
|
+
display: flex;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
align-items: center;
|
|
155
|
+
color: #f8fafc;
|
|
156
|
+
}
|
|
157
|
+
.error-card {
|
|
158
|
+
background: #1e293b;
|
|
159
|
+
border: 1px solid #334155;
|
|
160
|
+
border-radius: 12px;
|
|
161
|
+
padding: 2.5rem;
|
|
162
|
+
text-align: center;
|
|
163
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
|
|
164
|
+
max-width: 450px;
|
|
165
|
+
width: 90%;
|
|
166
|
+
}
|
|
167
|
+
.error-code {
|
|
168
|
+
font-size: 5rem;
|
|
169
|
+
font-weight: 800;
|
|
170
|
+
background: linear-gradient(135deg, #38bdf8 0%, #818cf8 100%);
|
|
171
|
+
-webkit-background-clip: text;
|
|
172
|
+
background-clip: text;
|
|
173
|
+
color: transparent;
|
|
174
|
+
line-height: 1;
|
|
175
|
+
margin-bottom: 0.5rem;
|
|
176
|
+
}
|
|
177
|
+
h1 { font-size: 1.5rem; font-weight: 600; color: #f1f5f9; margin-bottom: 0.75rem; }
|
|
178
|
+
p { color: #94a3b8; font-size: 0.95rem; line-height: 1.5; margin-bottom: 1.5rem; }
|
|
179
|
+
.btn-back {
|
|
180
|
+
display: inline-block;
|
|
181
|
+
background: linear-gradient(135deg, #38bdf8 0%, #6366f1 100%);
|
|
182
|
+
color: #ffffff;
|
|
183
|
+
text-decoration: none;
|
|
184
|
+
font-weight: 500;
|
|
185
|
+
font-size: 0.9rem;
|
|
186
|
+
padding: 0.75rem 1.5rem;
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
transition: transform 0.2s, opacity 0.2s;
|
|
189
|
+
}
|
|
190
|
+
.btn-back:hover { transform: translateY(-1px); opacity: 0.95; }
|
|
191
|
+
</style>
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
<div class="error-card">
|
|
195
|
+
<div class="error-code">404</div>
|
|
196
|
+
<h1>Archivo no encontrado</h1>
|
|
197
|
+
<p>El recurso que estás intentando cargar no existe en el directorio local o la ruta es incorrecta.</p>
|
|
198
|
+
<a href="/" class="btn-back">Volver al inicio</a>
|
|
199
|
+
</div>
|
|
200
|
+
</body>
|
|
201
|
+
</html>
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
initWebSocket(server);
|
|
207
|
+
|
|
208
|
+
state.server = server;
|
|
209
|
+
server.listen(config.port || 3003);
|
|
210
|
+
console.log(styleText("bgGreen", " servidor ") + ` corriendo en http://localhost:${config.port || 3003}`);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (config.outputPath === "log") {
|
|
213
|
+
if (!existsSync(join(state.currentDirectory, 'logs'))) {
|
|
214
|
+
mkdirSync(join(state.currentDirectory, 'logs'), { recursive: true });
|
|
215
|
+
}
|
|
216
|
+
let messageError = `${Date.now()} - ${hostname()}/${state.currentDirectory}: ${error.message}`;
|
|
217
|
+
await write(join(state.currentDirectory, 'logs', 'errors.log'), messageError);
|
|
218
|
+
}
|
|
219
|
+
console.log(styleText("bgRed", " error: ") + error.message);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function reloadServer() {
|
|
225
|
+
if (state.isRestarting) return;
|
|
226
|
+
state.isRestarting = true;
|
|
227
|
+
|
|
228
|
+
console.log(styleText("bgYellow", " reseteando ") + " Reiniciando PulseDev...");
|
|
229
|
+
|
|
230
|
+
_reloadCount++;
|
|
231
|
+
|
|
232
|
+
closeAllConnections();
|
|
233
|
+
|
|
234
|
+
if (state.server) {
|
|
235
|
+
await new Promise((resolve) => state.server.close(() => resolve()));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const rawConfig = await read(join(state.currentDirectory, 'pulsedev.json'));
|
|
240
|
+
if (!rawConfig) {
|
|
241
|
+
console.log(styleText("bgRed", " error ") + " No se pudo leer pulsedev.json al reiniciar.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
state.config = JSON.parse(rawConfig);
|
|
245
|
+
await upServer(state.config);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error("Error al reiniciar el servidor:", error.message);
|
|
248
|
+
} finally {
|
|
249
|
+
state.isRestarting = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function startServer(currentDirectory, preloadedConfig = null) {
|
|
254
|
+
state.currentDirectory = currentDirectory;
|
|
255
|
+
|
|
256
|
+
if (preloadedConfig) {
|
|
257
|
+
state.config = preloadedConfig;
|
|
258
|
+
} else {
|
|
259
|
+
if (!existsSync(join(currentDirectory, 'pulsedev.json'))) {
|
|
260
|
+
console.log(styleText("bgRed", " warning: ") + "El archivo pulsedev.json no existe en el directorio.");
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
state.config = JSON.parse(await read(join(currentDirectory, 'pulsedev.json')));
|
|
265
|
+
}
|
|
266
|
+
await upServer(state.config);
|
|
267
|
+
|
|
268
|
+
// Abortar watchers previos antes de crear nuevos (evita fugas de event loop)
|
|
269
|
+
if (state.watcherAbortController) {
|
|
270
|
+
state.watcherAbortController.abort();
|
|
271
|
+
state.watcherAbortController = null;
|
|
272
|
+
}
|
|
273
|
+
await _closeActiveWatchers();
|
|
274
|
+
state.watcherAbortController = new AbortController();
|
|
275
|
+
startWatcher(state.config, currentDirectory, reloadServer, state.watcherAbortController.signal);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export { reloadServer };
|
|
279
|
+
|
|
280
|
+
// exported for testing
|
|
281
|
+
export async function closeServer() {
|
|
282
|
+
closeAllConnections();
|
|
283
|
+
|
|
284
|
+
if (state.watcherAbortController) {
|
|
285
|
+
state.watcherAbortController.abort();
|
|
286
|
+
state.watcherAbortController = null;
|
|
287
|
+
}
|
|
288
|
+
await _closeActiveWatchers();
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
if (state.server) {
|
|
291
|
+
state.server.closeAllConnections?.();
|
|
292
|
+
state.server.close(() => resolve());
|
|
293
|
+
} else {
|
|
294
|
+
resolve();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
export function getReloadCount() { return _reloadCount; }
|
|
299
|
+
export function resetReloadCount() { _reloadCount = 0; }
|