versacompiler 2.5.0 → 2.6.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.
Files changed (49) hide show
  1. package/dist/compiler/compile-worker-pool.js +12 -0
  2. package/dist/compiler/compile.js +97 -17
  3. package/dist/compiler/error-reporter.js +12 -0
  4. package/dist/compiler/integrity-validator.js +12 -0
  5. package/dist/compiler/linter.js +12 -0
  6. package/dist/compiler/minify.js +12 -0
  7. package/dist/compiler/minifyTemplate.js +12 -0
  8. package/dist/compiler/module-resolution-optimizer.js +13 -1
  9. package/dist/compiler/parser.js +12 -0
  10. package/dist/compiler/performance-monitor.js +12 -0
  11. package/dist/compiler/pipeline/build-pipeline.js +12 -0
  12. package/dist/compiler/pipeline/core-plugins.js +12 -0
  13. package/dist/compiler/pipeline/module-graph.js +12 -0
  14. package/dist/compiler/pipeline/plugin-driver.js +12 -0
  15. package/dist/compiler/pipeline/types.js +12 -0
  16. package/dist/compiler/tailwindcss.js +12 -0
  17. package/dist/compiler/transform-optimizer.js +12 -0
  18. package/dist/compiler/transformTStoJS.js +38 -5
  19. package/dist/compiler/transforms.js +12 -0
  20. package/dist/compiler/typescript-compiler.js +12 -0
  21. package/dist/compiler/typescript-error-parser.js +12 -0
  22. package/dist/compiler/typescript-manager.js +12 -0
  23. package/dist/compiler/typescript-sync-validator.js +12 -0
  24. package/dist/compiler/typescript-worker-pool.js +12 -0
  25. package/dist/compiler/typescript-worker.js +12 -0
  26. package/dist/compiler/vuejs.js +41 -15
  27. package/dist/config.js +12 -0
  28. package/dist/hrm/VueHRM.js +132 -7
  29. package/dist/hrm/errorScreen.js +12 -0
  30. package/dist/hrm/getInstanciaVue.js +12 -0
  31. package/dist/hrm/initHRM.js +117 -9
  32. package/dist/hrm/versaHMR.js +317 -0
  33. package/dist/main.js +21 -2
  34. package/dist/servicios/browserSync.js +119 -4
  35. package/dist/servicios/file-watcher.js +104 -15
  36. package/dist/servicios/logger.js +12 -0
  37. package/dist/servicios/readConfig.js +13 -1
  38. package/dist/servicios/versacompile.config.types.js +12 -0
  39. package/dist/utils/excluded-modules.js +12 -0
  40. package/dist/utils/module-resolver.js +12 -0
  41. package/dist/utils/promptUser.js +12 -0
  42. package/dist/utils/proxyValidator.js +12 -0
  43. package/dist/utils/resolve-bin.js +12 -0
  44. package/dist/utils/utils.js +12 -0
  45. package/dist/utils/vue-types-setup.js +14 -2
  46. package/dist/wrappers/eslint-node.js +12 -0
  47. package/dist/wrappers/oxlint-node.js +12 -0
  48. package/dist/wrappers/tailwind-node.js +12 -0
  49. package/package.json +4 -3
@@ -0,0 +1,317 @@
1
+ /* VersaCompiler HMR shim [dev] */
2
+ if (typeof window !== 'undefined' && window.__versaHMR) {
3
+ (() => {
4
+ const _id = new URL(import.meta.url).pathname;
5
+ import.meta.hot = {
6
+ accept(cb) { window.__versaHMR.accept(_id, typeof cb === 'function' ? cb : () => {}); },
7
+ invalidate() { window.__versaHMR._invalidate?.(_id); },
8
+ dispose(cb) { window.__versaHMR._onDispose?.(_id, cb); },
9
+ get data() { return window.__versaHMR._getHotData?.(_id) ?? {}; },
10
+ };
11
+ })();
12
+ }
13
+ /**
14
+ * @fileoverview VersaHMR — Registry de módulos para Hot Module Replacement
15
+ *
16
+ * Permite que librerías y utilidades JS/TS reciban actualizaciones sin full-reload.
17
+ *
18
+ * Uso básico (en cualquier módulo que consuma una librería):
19
+ *
20
+ * window.__versaHMR.accept('/public/utils/math.js', (newModule) => {
21
+ * // Recibir la nueva versión y actualizar referencias locales
22
+ * mathLib = newModule;
23
+ * });
24
+ *
25
+ * Uso automático (para módulos que solo exportan funciones/constantes):
26
+ * El sistema detecta automáticamente si es seguro hacer HMR y llama a los callbacks.
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} ModuleEntry
31
+ * @property {any} module - La instancia actual del módulo
32
+ * @property {Set<Function>} callbacks - Callbacks registrados para actualizaciones
33
+ * @property {number} version - Versión (incrementa en cada update)
34
+ */
35
+
36
+ class VersaModuleRegistry {
37
+ constructor() {
38
+ /** @type {Map<string, ModuleEntry>} */
39
+ this._registry = new Map();
40
+
41
+ /** @type {Map<string, Set<string>>} Árbol inverso: módulo → quién lo importa */
42
+ this._importers = new Map();
43
+
44
+ /** @type {Set<string>} Módulos que han fallado al actualizarse */
45
+ this._failedUpdates = new Set();
46
+
47
+ /** @type {Map<string, Set<Function>>} Callbacks de cleanup por módulo */
48
+ this._disposeCallbacks = new Map();
49
+
50
+ /** @type {Map<string, Object>} Datos persistentes entre ciclos HMR */
51
+ this._hotData = new Map();
52
+ }
53
+
54
+ /**
55
+ * Normaliza un moduleId para uso como clave en el registry.
56
+ * Elimina query params y asegura formato consistente.
57
+ * @param {string} moduleId
58
+ * @returns {string}
59
+ */
60
+ _normalizeId(moduleId) {
61
+ try {
62
+ // Si es una URL relativa, la convertimos a pathname limpio
63
+ const url = new URL(moduleId, window.location.origin);
64
+ return url.pathname;
65
+ } catch {
66
+ return moduleId.split('?')[0];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Registra un callback para recibir actualizaciones de un módulo.
72
+ * El callback se llama con (newModule, { moduleId, version }) cuando el módulo cambia.
73
+ *
74
+ * @param {string} moduleId - Path/URL del módulo a observar (ej: '/public/utils/math.js')
75
+ * @param {Function} updateCallback - Función llamada cuando el módulo cambia
76
+ * @returns {Function} Función para cancelar el registro (unsubscribe)
77
+ */
78
+ accept(moduleId, updateCallback) {
79
+ const id = this._normalizeId(moduleId);
80
+
81
+ if (!this._registry.has(id)) {
82
+ this._registry.set(id, {
83
+ module: null,
84
+ callbacks: new Set(),
85
+ version: 0,
86
+ });
87
+ }
88
+
89
+ const entry = this._registry.get(id);
90
+ entry.callbacks.add(updateCallback);
91
+
92
+ console.log(`[VersaHMR] Registrado observer para: ${id}`);
93
+
94
+ // Retornar función de cleanup
95
+ return () => {
96
+ const e = this._registry.get(id);
97
+ if (e) {
98
+ e.callbacks.delete(updateCallback);
99
+ if (e.callbacks.size === 0) {
100
+ this._registry.delete(id);
101
+ console.log(`[VersaHMR] Registry limpiado para: ${id}`);
102
+ }
103
+ }
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Notifica a todos los observers registrados que un módulo fue actualizado.
109
+ * Llamado internamente por el handler de HRMHelper cuando recibe un nuevo módulo.
110
+ *
111
+ * @param {string} moduleId - Path del módulo actualizado
112
+ * @param {any} newModule - La nueva instancia del módulo exportado
113
+ * @returns {boolean} true si había al menos un observer y todos ejecutaron sin error
114
+ */
115
+ notifyUpdate(moduleId, newModule) {
116
+ const id = this._normalizeId(moduleId);
117
+ const entry = this._registry.get(id);
118
+
119
+ if (!entry || entry.callbacks.size === 0) {
120
+ return false;
121
+ }
122
+
123
+ // Ejecutar dispose callbacks antes de actualizar
124
+ this._runDispose(id);
125
+
126
+ entry.version++;
127
+ entry.module = newModule;
128
+ const meta = { moduleId: id, version: entry.version };
129
+
130
+ let allSucceeded = true;
131
+
132
+ for (const callback of entry.callbacks) {
133
+ try {
134
+ callback(newModule, meta);
135
+ } catch (err) {
136
+ console.error(
137
+ `[VersaHMR] Error en callback de update para ${id}:`,
138
+ err,
139
+ );
140
+ allSucceeded = false;
141
+ this._failedUpdates.add(id);
142
+ }
143
+ }
144
+
145
+ if (allSucceeded) {
146
+ this._failedUpdates.delete(id);
147
+ console.log(
148
+ `[VersaHMR] ✅ Módulo actualizado (v${entry.version}): ${id} — ${entry.callbacks.size} observer(s) notificado(s)`,
149
+ );
150
+ }
151
+
152
+ return allSucceeded;
153
+ }
154
+
155
+ /**
156
+ * Registra la relación de importación entre módulos.
157
+ * Esto permite propagación en cadena cuando cambia un módulo.
158
+ *
159
+ * @param {string} importer - Path del módulo que importa
160
+ * @param {string} imported - Path del módulo importado
161
+ */
162
+ registerImporter(importer, imported) {
163
+ const importedId = this._normalizeId(imported);
164
+ const importerId = this._normalizeId(importer);
165
+
166
+ if (!this._importers.has(importedId)) {
167
+ this._importers.set(importedId, new Set());
168
+ }
169
+ this._importers.get(importedId).add(importerId);
170
+ }
171
+
172
+ /**
173
+ * Obtiene el módulo actual registrado (si fue cargado via notifyUpdate).
174
+ * @param {string} moduleId
175
+ * @returns {any | null}
176
+ */
177
+ getModule(moduleId) {
178
+ const id = this._normalizeId(moduleId);
179
+ return this._registry.get(id)?.module ?? null;
180
+ }
181
+
182
+ /**
183
+ * Registra un callback de dispose que se ejecuta antes de que el módulo
184
+ * sea reemplazado. Permite cleanup de timers, event listeners, etc.
185
+ * Expuesto como import.meta.hot.dispose(cb) en el shim de HMR.
186
+ *
187
+ * @param {string} moduleId
188
+ * @param {Function} cb - Llamado con (data) donde data = _getHotData(moduleId)
189
+ */
190
+ _onDispose(moduleId, cb) {
191
+ const id = this._normalizeId(moduleId);
192
+ if (!this._disposeCallbacks.has(id)) {
193
+ this._disposeCallbacks.set(id, new Set());
194
+ }
195
+ this._disposeCallbacks.get(id).add(cb);
196
+ }
197
+
198
+ /**
199
+ * Retorna el objeto de datos persistentes de un módulo.
200
+ * El objeto sobrevive entre ciclos HMR para preservar estado local.
201
+ * Expuesto como import.meta.hot.data en el shim de HMR.
202
+ *
203
+ * @param {string} moduleId
204
+ * @returns {Object}
205
+ */
206
+ _getHotData(moduleId) {
207
+ const id = this._normalizeId(moduleId);
208
+ if (!this._hotData.has(id)) {
209
+ this._hotData.set(id, {});
210
+ }
211
+ return this._hotData.get(id);
212
+ }
213
+
214
+ /**
215
+ * Ejecuta los callbacks de dispose antes de un update HMR.
216
+ * Llamado internamente desde notifyUpdate antes de notificar observers.
217
+ *
218
+ * @param {string} moduleId
219
+ */
220
+ _runDispose(moduleId) {
221
+ const id = this._normalizeId(moduleId);
222
+ const disposers = this._disposeCallbacks.get(id);
223
+ if (!disposers || disposers.size === 0) return;
224
+
225
+ const data = this._getHotData(id);
226
+ for (const cb of disposers) {
227
+ try {
228
+ cb(data);
229
+ } catch (err) {
230
+ console.error(
231
+ `[VersaHMR] Error en dispose callback para ${id}:`,
232
+ err,
233
+ );
234
+ }
235
+ }
236
+ this._disposeCallbacks.delete(id);
237
+ }
238
+
239
+ /**
240
+ * Invalida un módulo: ejecuta dispose callbacks y fuerza recarga de página.
241
+ * Expuesto como import.meta.hot.invalidate() en el shim de HMR.
242
+ *
243
+ * @param {string} moduleId
244
+ */
245
+ _invalidate(moduleId) {
246
+ const id = this._normalizeId(moduleId);
247
+
248
+ // Ejecutar dispose callbacks antes de invalidar
249
+ this._runDispose(id);
250
+
251
+ // Limpiar estado del módulo
252
+ this._hotData.delete(id);
253
+ this._registry.delete(id);
254
+
255
+ console.log(
256
+ `[VersaHMR] Módulo invalidado: ${id} — forzando recarga completa`,
257
+ );
258
+ window.location.reload();
259
+ }
260
+
261
+ /**
262
+ * Indica si un módulo tiene observers registrados (puede hacer HMR).
263
+ * @param {string} moduleId
264
+ * @returns {boolean}
265
+ */
266
+ hasObservers(moduleId) {
267
+ const id = this._normalizeId(moduleId);
268
+ const entry = this._registry.get(id);
269
+ return !!(entry && entry.callbacks.size > 0);
270
+ }
271
+
272
+ /**
273
+ * Indica si un módulo tuvo fallos en el último update.
274
+ * @param {string} moduleId
275
+ * @returns {boolean}
276
+ */
277
+ hasFailed(moduleId) {
278
+ return this._failedUpdates.has(this._normalizeId(moduleId));
279
+ }
280
+
281
+ /**
282
+ * Limpia todos los registros (útil para tests o hot-reload del propio sistema).
283
+ */
284
+ clear() {
285
+ this._registry.clear();
286
+ this._importers.clear();
287
+ this._failedUpdates.clear();
288
+ this._disposeCallbacks.clear();
289
+ this._hotData.clear();
290
+ }
291
+
292
+ /**
293
+ * Estadísticas del registry (para debugging).
294
+ * @returns {{ modules: number, totalObservers: number, failedModules: number }}
295
+ */
296
+ getStats() {
297
+ let totalObservers = 0;
298
+ for (const entry of this._registry.values()) {
299
+ totalObservers += entry.callbacks.size;
300
+ }
301
+ return {
302
+ modules: this._registry.size,
303
+ totalObservers,
304
+ failedModules: this._failedUpdates.size,
305
+ };
306
+ }
307
+ }
308
+
309
+ // Singleton global expuesto en window.__versaHMR
310
+ // Se preserva entre recargas parciales para no perder observers
311
+ if (!window.__versaHMR) {
312
+ window.__versaHMR = new VersaModuleRegistry();
313
+ console.log('[VersaHMR] Registry inicializado');
314
+ }
315
+
316
+ export const versaHMR = window.__versaHMR;
317
+ export default versaHMR;
package/dist/main.js CHANGED
@@ -1,3 +1,15 @@
1
+ /* VersaCompiler HMR shim [dev] */
2
+ if (typeof window !== 'undefined' && window.__versaHMR) {
3
+ (() => {
4
+ const _id = new URL(import.meta.url).pathname;
5
+ import.meta.hot = {
6
+ accept(cb) { window.__versaHMR.accept(_id, typeof cb === 'function' ? cb : () => {}); },
7
+ invalidate() { window.__versaHMR._invalidate?.(_id); },
8
+ dispose(cb) { window.__versaHMR._onDispose?.(_id, cb); },
9
+ get data() { return window.__versaHMR._getHotData?.(_id) ?? {}; },
10
+ };
11
+ })();
12
+ }
1
13
  #!/usr/bin/env node
2
14
  import * as path from 'node:path';
3
15
  import * as processModule from 'node:process';
@@ -259,6 +271,13 @@ async function main() {
259
271
  try {
260
272
  // Verificar si el archivo existe
261
273
  await fs.access(file);
274
+ }
275
+ catch {
276
+ logger.error(chalk.red(`❌ El archivo '${file}' no existe.`));
277
+ hasErrors = true;
278
+ continue;
279
+ }
280
+ try {
262
281
  logger.info(chalk.blue(`🔄 Compilando: ${file}`));
263
282
  const result = await compileFile(file);
264
283
  if (result.success) {
@@ -269,8 +288,8 @@ async function main() {
269
288
  hasErrors = true;
270
289
  }
271
290
  }
272
- catch {
273
- logger.error(chalk.red(`❌ El archivo '${file}' no existe.`));
291
+ catch (err) {
292
+ logger.error(chalk.red(`❌ Error al compilar '${file}': ${err instanceof Error ? err.message : String(err)}`));
274
293
  hasErrors = true;
275
294
  }
276
295
  }
@@ -1,3 +1,17 @@
1
+ /* VersaCompiler HMR shim [dev] */
2
+ if (typeof window !== 'undefined' && window.__versaHMR) {
3
+ (() => {
4
+ const _id = new URL(import.meta.url).pathname;
5
+ import.meta.hot = {
6
+ accept(cb) { window.__versaHMR.accept(_id, typeof cb === 'function' ? cb : () => {}); },
7
+ invalidate() { window.__versaHMR._invalidate?.(_id); },
8
+ dispose(cb) { window.__versaHMR._onDispose?.(_id, cb); },
9
+ get data() { return window.__versaHMR._getHotData?.(_id) ?? {}; },
10
+ };
11
+ window.__versaHMR.accept("/path/to/file.js", () => { import(_id + '?t=' + Date.now()); });
12
+ window.__versaHMR.accept("/public/js/bar.js", () => { import(_id + '?t=' + Date.now()); });
13
+ })();
14
+ }
1
15
  import { createHash } from 'node:crypto';
2
16
  import { promises as fs } from 'node:fs';
3
17
  import * as path from 'node:path';
@@ -315,6 +329,54 @@ class BrowserSyncFileCache {
315
329
  }
316
330
  // Instancia global del cache de archivos
317
331
  const fileCache = BrowserSyncFileCache.getInstance();
332
+ // ─── HMR Import Rewriting ─────────────────────────────────────────────────────
333
+ // Cuando un módulo se re-compila en watch mode, registramos su path con timestamp.
334
+ // Al servir requests con ?t= (re-imports HMR), reescribimos los static imports de
335
+ // ese archivo para que también incluyan ?t=, forzando al browser a cargar la nueva
336
+ // versión en lugar de usar el módulo cacheado en su module registry.
337
+ /** Map de path normalizado → timestamp de la última compilación exitosa */
338
+ const recentHMRTimestamps = new Map();
339
+ /** TTL para limpiar entradas viejas del mapa (5 minutos) */
340
+ const HMR_TIMESTAMP_TTL = 5 * 60 * 1000;
341
+ /**
342
+ * Registra que un output file fue re-compilado en modo watch.
343
+ * Debe llamarse inmediatamente después de escribir el archivo a disco.
344
+ * @param outputPath - Path relativo del output compilado (ej: 'public/js/sampleFile.js')
345
+ */
346
+ export function registerHMRUpdate(outputPath) {
347
+ // Normalizar a path absoluto con / inicial (como lo ve el browser)
348
+ const normalized = '/' + outputPath.replace(/\\/g, '/').replace(/^\.?\/+/, '');
349
+ recentHMRTimestamps.set(normalized, Date.now());
350
+ // Limpiar entradas viejas para evitar crecimiento ilimitado
351
+ const cutoff = Date.now() - HMR_TIMESTAMP_TTL;
352
+ for (const [key, ts] of recentHMRTimestamps) {
353
+ if (ts < cutoff)
354
+ recentHMRTimestamps.delete(key);
355
+ }
356
+ }
357
+ /**
358
+ * Reescribe los static imports de un archivo JS para agregar ?t=<timestamp>
359
+ * a cualquier módulo que haya sido recientemente re-compilado.
360
+ * Esto fuerza al browser a crear un nuevo module record (URL diferente)
361
+ * en lugar de reusar el módulo cacheado del registry.
362
+ */
363
+ function rewriteImportsForHMR(content) {
364
+ if (recentHMRTimestamps.size === 0)
365
+ return content;
366
+ // Reescribir: from '/path/to/file.js' → from '/path/to/file.js?t=<ts>'
367
+ // Handles: import ... from '...', export ... from '...'
368
+ // Solo paths absolutos (con /) → son los compilados por VersaCompiler
369
+ return content.replace(/((?:from|import)\s+['"])(\/.+?\.js)(['""])/g, (_match, prefix, importPath, suffix) => {
370
+ // Quitar ?t= previo si ya existe
371
+ const cleanPath = importPath.split('?')[0];
372
+ const ts = recentHMRTimestamps.get(cleanPath);
373
+ if (ts) {
374
+ return `${prefix}${cleanPath}?t=${ts}${suffix}`;
375
+ }
376
+ return _match;
377
+ });
378
+ }
379
+ // ─────────────────────────────────────────────────────────────────────────────
318
380
  // Lazy loading para chalk - pre-cargado para mejor rendimiento
319
381
  let chalk;
320
382
  let chalkPromise = null;
@@ -388,6 +450,9 @@ export async function browserSyncServer() {
388
450
  fn: (snippet, match) => {
389
451
  return `
390
452
  ${snippet}${match}
453
+ <script
454
+ type="module"
455
+ src="/__versa/versaHMR.js"></script>
391
456
  <script
392
457
  type="module"
393
458
  src="/__versa/initHRM.js"></script>
@@ -447,8 +512,19 @@ export async function browserSyncServer() {
447
512
  }
448
513
  // Si la URL comienza con /__versa/hrm/, sirve los archivos de dist/hrm
449
514
  if (req.url.startsWith('/__versa/')) {
450
- // ✨ OPTIMIZADO: Usar cache para archivos Versa
451
- const filePath = path.join(relativeHrmPath, req.url.replace('/__versa/', ''));
515
+ // ✨ SEGURIDAD: Prevenir path traversal normalizando y verificando el directorio
516
+ const requestedRelative = req.url
517
+ .replace('/__versa/', '')
518
+ .split('?')[0]; // strip query string
519
+ const resolvedFilePath = path.resolve(relativeHrmPath, requestedRelative);
520
+ const allowedBase = path.resolve(relativeHrmPath);
521
+ if (!resolvedFilePath.startsWith(allowedBase + path.sep) &&
522
+ resolvedFilePath !== allowedBase) {
523
+ res.statusCode = 403;
524
+ res.end('// Forbidden');
525
+ return;
526
+ }
527
+ const filePath = resolvedFilePath;
452
528
  const cachedFile = await fileCache.getOrReadFile(filePath);
453
529
  if (cachedFile) {
454
530
  res.setHeader('Content-Type', cachedFile.contentType);
@@ -473,8 +549,19 @@ export async function browserSyncServer() {
473
549
  }
474
550
  // Si la URL comienza con /node_modules/, sirve los archivos de node_modules
475
551
  if (req.url.startsWith('/node_modules/')) {
476
- // ✨ OPTIMIZADO: Usar cache para módulos de node_modules
477
- const modulePath = path.join(process.cwd(), req.url);
552
+ // ✨ SEGURIDAD: Prevenir path traversal verificando que el path resuelto
553
+ // permanece dentro de node_modules del proyecto
554
+ const requestedRelative = req.url
555
+ .replace('/node_modules/', '')
556
+ .split('?')[0]; // strip query string
557
+ const nodeModulesBase = path.resolve(process.cwd(), 'node_modules');
558
+ const modulePath = path.resolve(nodeModulesBase, requestedRelative);
559
+ if (!modulePath.startsWith(nodeModulesBase + path.sep) &&
560
+ modulePath !== nodeModulesBase) {
561
+ res.statusCode = 403;
562
+ res.end('// Forbidden');
563
+ return;
564
+ }
478
565
  const cachedFile = await fileCache.getOrReadFile(modulePath);
479
566
  if (cachedFile) {
480
567
  res.setHeader('Content-Type', cachedFile.contentType);
@@ -497,6 +584,34 @@ export async function browserSyncServer() {
497
584
  }
498
585
  return;
499
586
  }
587
+ // ── HMR re-import: reescribir imports para que el browser no use caché ──
588
+ // Cuando el cliente hace import('/public/js/foo.js?t=123'), el browser crea
589
+ // un nuevo module record para esa URL. Al evaluar el módulo, sus static imports
590
+ // (ej: import { x } from '/public/js/bar.js') SIN timestamp reusan el módulo
591
+ // cacheado del registry → se ve el código viejo.
592
+ // Solución: interceptar estas requests y reescribir los imports del archivo
593
+ // para que también lleven ?t= en los módulos recientemente modificados.
594
+ const isHMRReimport = req.method === 'GET' &&
595
+ req.url.includes('?t=') &&
596
+ /\.js\?/.test(req.url) &&
597
+ !req.url.startsWith('/__versa/') &&
598
+ !req.url.startsWith('/node_modules/');
599
+ if (isHMRReimport) {
600
+ const urlPath = req.url.split('?')[0];
601
+ const filePath = path.join(process.cwd(), urlPath);
602
+ try {
603
+ const rawContent = await fs.readFile(filePath, 'utf-8');
604
+ const rewritten = rewriteImportsForHMR(rawContent);
605
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
606
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
607
+ res.end(rewritten);
608
+ return;
609
+ }
610
+ catch {
611
+ // Archivo no encontrado, seguir al handler estático
612
+ }
613
+ }
614
+ // ──────────────────────────────────────────────────────────────────────────
500
615
  // detectar si es un archivo estático, puede que contenga un . y alguna extensión o dashUsers.js?v=1746559083866
501
616
  const isAssets = req.url.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|map|webp|avif|json|html|xml|txt|pdf|zip|mp4|mp3|wav|ogg)(\?.*)?$/i);
502
617
  if (req.method === 'GET') {
@@ -1,4 +1,16 @@
1
- import { readdir, rm, stat, unlink } from 'node:fs/promises';
1
+ /* VersaCompiler HMR shim [dev] */
2
+ if (typeof window !== 'undefined' && window.__versaHMR) {
3
+ (() => {
4
+ const _id = new URL(import.meta.url).pathname;
5
+ import.meta.hot = {
6
+ accept(cb) { window.__versaHMR.accept(_id, typeof cb === 'function' ? cb : () => {}); },
7
+ invalidate() { window.__versaHMR._invalidate?.(_id); },
8
+ dispose(cb) { window.__versaHMR._onDispose?.(_id, cb); },
9
+ get data() { return window.__versaHMR._getHotData?.(_id) ?? {}; },
10
+ };
11
+ })();
12
+ }
13
+ import { readdir, readFile, rm, stat, unlink } from 'node:fs/promises';
2
14
  import * as path from 'node:path';
3
15
  import * as process from 'node:process';
4
16
  const { env } = process;
@@ -6,7 +18,7 @@ import * as chokidar from 'chokidar';
6
18
  import { minimatch } from 'minimatch';
7
19
  import { clearCompilationState, getOutputPath, getPipelineModuleGraph, initCompile, normalizeRuta, runPipelineHotUpdate, } from '../compiler/compile.js';
8
20
  import { promptUser } from '../utils/promptUser.js';
9
- import { emitirCambios } from './browserSync.js';
21
+ import { emitirCambios, registerHMRUpdate } from './browserSync.js';
10
22
  import { logger } from './logger.js';
11
23
  // Lazy loading para chalk
12
24
  let chalk;
@@ -16,6 +28,49 @@ async function loadChalk() {
16
28
  }
17
29
  return chalk;
18
30
  }
31
+ /**
32
+ * Analiza el contenido de un módulo JS compilado para determinar si es seguro
33
+ * hacer HMR sin full-reload. Un módulo es "simple" si:
34
+ * - Solo exporta funciones, constantes o valores (no clases con estado ni patrones singleton)
35
+ * - No usa new/class en el scope raíz (como estado global mutable)
36
+ *
37
+ * @param outputPath - Ruta del archivo compilado a analizar
38
+ * @returns 'propagate' si es seguro sin full-reload, 'full-reload' si no se puede determinar
39
+ */
40
+ async function analyzeCompiledModuleStrategy(outputPath) {
41
+ try {
42
+ const content = await readFile(outputPath, 'utf8');
43
+ // Si el módulo fue compilado por VersaCompiler en modo dev, el shim HMR
44
+ // está inyectado y gestiona la estrategia de reemplazo vía versaHMR.
45
+ // No analizar el shim como side-effect — siempre es propagate.
46
+ if (content.startsWith('/* VersaCompiler HMR shim [dev] */')) {
47
+ return 'propagate';
48
+ }
49
+ // Si el módulo declara que acepta HMR, propagate es seguro
50
+ if (/import\.meta\.hot\.accept/.test(content)) {
51
+ return 'propagate';
52
+ }
53
+ // Heurística: módulos con solo exports de funciones/constantes son seguros
54
+ const hasExports = /export\s+(const|let|function|async function|class\b)/.test(content) || /export\s+default/.test(content);
55
+ if (!hasExports) {
56
+ return 'full-reload';
57
+ }
58
+ // Señales de estado global mutable o efectos secundarios al import:
59
+ // new ClassName() en scope raíz, módulos con side-effects de init
60
+ const hasRootLevelSideEffects =
61
+ // new en scope raíz (fuera de funciones/clases) — detectar heurísticamente
62
+ /^\s*(?:const|let|var)\s+\w+\s*=\s*new\s+\w+/m.test(content) ||
63
+ // Llamadas a funciones en scope raíz que sugieren init
64
+ /^\s*(?:init|setup|bootstrap|start|connect|register)\s*\(/m.test(content);
65
+ if (hasRootLevelSideEffects) {
66
+ return 'full-reload';
67
+ }
68
+ return 'propagate';
69
+ }
70
+ catch {
71
+ return 'full-reload';
72
+ }
73
+ }
19
74
  class WatchDebouncer {
20
75
  pendingChanges = new Map();
21
76
  debounceTimer = null;
@@ -162,11 +217,23 @@ class WatchDebouncer {
162
217
  for (const invalidatedPath of invalidated) {
163
218
  if (expandedChanges.has(invalidatedPath))
164
219
  continue;
220
+ // Usar la acción correcta según la extensión del archivo invalidado
221
+ // en lugar de forzar 'reloadFull' para todos los importers
222
+ const invalidatedExt = path
223
+ .extname(invalidatedPath)
224
+ .replace('.', '');
225
+ const invalidatedAction = {
226
+ vue: 'HRMVue',
227
+ ts: 'HRMHelper',
228
+ js: 'HRMHelper',
229
+ mjs: 'HRMHelper',
230
+ cjs: 'HRMHelper',
231
+ }[invalidatedExt] ?? 'reloadFull';
165
232
  expandedChanges.set(invalidatedPath, {
166
233
  filePath: invalidatedPath,
167
234
  action: 'change',
168
235
  timestamp: Date.now(),
169
- extensionAction: 'reloadFull',
236
+ extensionAction: invalidatedAction,
170
237
  isAdditionalFile: false,
171
238
  });
172
239
  }
@@ -203,6 +270,10 @@ class WatchDebouncer {
203
270
  }
204
271
  const result = await initCompile(change.filePath, true, 'watch');
205
272
  if (result.success) {
273
+ // Registrar el output para que el middleware HMR reescriba imports dependientes
274
+ if (result.output) {
275
+ registerHMRUpdate(result.output);
276
+ }
206
277
  let accion = result.action || change.extensionAction;
207
278
  accion =
208
279
  accion === 'extension' ? change.extensionAction : accion;
@@ -211,20 +282,38 @@ class WatchDebouncer {
211
282
  if (pluginHmrReload === 'full') {
212
283
  accion = 'reloadFull';
213
284
  }
214
- const graph = getPipelineModuleGraph();
215
- const node = graph?.getNode(change.filePath);
216
- const importers = node ? Array.from(node.importers) : [];
217
- const strategy = importers.length > 0 ? 'propagate' : 'full-reload';
218
- if (pluginHmrReload === 'module') {
219
- payload.strategy = 'propagate';
220
- }
221
285
  else {
222
- payload.strategy = strategy;
286
+ const graph = getPipelineModuleGraph();
287
+ const node = graph?.getNode(change.filePath);
288
+ const importers = node
289
+ ? Array.from(node.importers)
290
+ : [];
291
+ // Determinar estrategia base:
292
+ // Si el plugin dice 'module' → propagate garantizado
293
+ // Si hay importers conocidos → propagate (los consumers actualizarán sus refs)
294
+ // Si no hay importers → analizar el módulo compilado para detectar si es "simple"
295
+ let strategy;
296
+ if (pluginHmrReload === 'module') {
297
+ strategy = 'propagate';
298
+ }
299
+ else if (importers.length > 0) {
300
+ strategy = 'propagate';
301
+ }
302
+ else {
303
+ // Sin importers conocidos: analizar el output compilado
304
+ strategy = await analyzeCompiledModuleStrategy(result.output);
305
+ }
306
+ // moduleId: path del output relativo al proyecto para que el cliente
307
+ // pueda hacer lookup en VersaModuleRegistry
308
+ const moduleId = result.output.startsWith('/')
309
+ ? result.output
310
+ : `/${result.output}`;
311
+ payload = {
312
+ moduleId,
313
+ importers,
314
+ strategy,
315
+ };
223
316
  }
224
- payload = {
225
- importers,
226
- ...payload,
227
- };
228
317
  }
229
318
  emitirCambios(this.browserSyncInstance, accion || 'reloadFull', result.output, payload);
230
319
  }