versacompiler 2.4.1 → 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.
- package/README.md +722 -722
- package/dist/compiler/compile-worker-pool.js +108 -0
- package/dist/compiler/compile-worker-thread.cjs +72 -0
- package/dist/compiler/compile.js +177 -18
- package/dist/compiler/error-reporter.js +12 -0
- package/dist/compiler/integrity-validator.js +13 -1
- package/dist/compiler/linter.js +12 -0
- package/dist/compiler/minify.js +12 -0
- package/dist/compiler/minifyTemplate.js +12 -0
- package/dist/compiler/module-resolution-optimizer.js +35 -20
- package/dist/compiler/parser.js +12 -0
- package/dist/compiler/performance-monitor.js +73 -61
- package/dist/compiler/pipeline/build-pipeline.js +139 -0
- package/dist/compiler/pipeline/core-plugins.js +230 -0
- package/dist/compiler/pipeline/module-graph.js +75 -0
- package/dist/compiler/pipeline/plugin-driver.js +99 -0
- package/dist/compiler/pipeline/types.js +14 -0
- package/dist/compiler/tailwindcss.js +12 -0
- package/dist/compiler/transform-optimizer.js +12 -0
- package/dist/compiler/transformTStoJS.js +38 -5
- package/dist/compiler/transforms.js +234 -16
- package/dist/compiler/typescript-compiler.js +12 -0
- package/dist/compiler/typescript-error-parser.js +12 -0
- package/dist/compiler/typescript-manager.js +15 -1
- package/dist/compiler/typescript-sync-validator.js +45 -31
- package/dist/compiler/typescript-worker-pool.js +12 -0
- package/dist/compiler/typescript-worker-thread.cjs +482 -475
- package/dist/compiler/typescript-worker.js +12 -0
- package/dist/compiler/vuejs.js +73 -47
- package/dist/config.js +14 -0
- package/dist/hrm/VueHRM.js +484 -359
- package/dist/hrm/errorScreen.js +95 -83
- package/dist/hrm/getInstanciaVue.js +325 -313
- package/dist/hrm/initHRM.js +736 -586
- package/dist/hrm/versaHMR.js +317 -0
- package/dist/main.js +23 -3
- package/dist/servicios/browserSync.js +127 -6
- package/dist/servicios/file-watcher.js +139 -8
- package/dist/servicios/logger.js +12 -0
- package/dist/servicios/readConfig.js +141 -54
- package/dist/servicios/versacompile.config.types.js +14 -0
- package/dist/utils/excluded-modules.js +12 -0
- package/dist/utils/module-resolver.js +86 -40
- package/dist/utils/promptUser.js +12 -0
- package/dist/utils/proxyValidator.js +12 -0
- package/dist/utils/resolve-bin.js +12 -0
- package/dist/utils/utils.js +12 -0
- package/dist/utils/vue-types-setup.js +260 -248
- package/dist/wrappers/eslint-node.js +15 -1
- package/dist/wrappers/oxlint-node.js +15 -1
- package/dist/wrappers/tailwind-node.js +12 -0
- package/package.json +74 -54
|
@@ -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';
|
|
@@ -66,7 +78,8 @@ async function main() {
|
|
|
66
78
|
const chalkInstance = await loadChalk();
|
|
67
79
|
let yargInstance = yargsInstance(hideBinFn(globalProcess.argv))
|
|
68
80
|
.scriptName('versa')
|
|
69
|
-
.usage(chalkInstance.blue('VersaCompiler') +
|
|
81
|
+
.usage(chalkInstance.blue('VersaCompiler') +
|
|
82
|
+
' - Compilador de archivos Vue/TS/JS')
|
|
70
83
|
.option('init', {
|
|
71
84
|
type: 'boolean',
|
|
72
85
|
description: 'Inicializar la configuración',
|
|
@@ -258,6 +271,13 @@ async function main() {
|
|
|
258
271
|
try {
|
|
259
272
|
// Verificar si el archivo existe
|
|
260
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 {
|
|
261
281
|
logger.info(chalk.blue(`🔄 Compilando: ${file}`));
|
|
262
282
|
const result = await compileFile(file);
|
|
263
283
|
if (result.success) {
|
|
@@ -268,8 +288,8 @@ async function main() {
|
|
|
268
288
|
hasErrors = true;
|
|
269
289
|
}
|
|
270
290
|
}
|
|
271
|
-
catch {
|
|
272
|
-
logger.error(chalk.red(`❌
|
|
291
|
+
catch (err) {
|
|
292
|
+
logger.error(chalk.red(`❌ Error al compilar '${file}': ${err instanceof Error ? err.message : String(err)}`));
|
|
273
293
|
hasErrors = true;
|
|
274
294
|
}
|
|
275
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
|
-
// ✨
|
|
451
|
-
const
|
|
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
|
-
// ✨
|
|
477
|
-
|
|
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') {
|
|
@@ -565,11 +680,17 @@ export async function browserSyncServer() {
|
|
|
565
680
|
process.exit(1);
|
|
566
681
|
}
|
|
567
682
|
}
|
|
568
|
-
export async function emitirCambios(bs, action, filePath) {
|
|
683
|
+
export async function emitirCambios(bs, action, filePath, payload = {}) {
|
|
569
684
|
// ✨ OPTIMIZACIÓN: Emitir PRIMERO (crítico), logging DESPUÉS (no crítico)
|
|
570
685
|
const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');
|
|
571
686
|
const nameFile = path.basename(normalizedPath, path.extname(normalizedPath));
|
|
572
|
-
bs.sockets.emit(action, {
|
|
687
|
+
bs.sockets.emit(action, {
|
|
688
|
+
action,
|
|
689
|
+
filePath,
|
|
690
|
+
normalizedPath,
|
|
691
|
+
nameFile,
|
|
692
|
+
...payload,
|
|
693
|
+
});
|
|
573
694
|
// Logging asíncrono para no bloquear la emisión
|
|
574
695
|
setImmediate(async () => {
|
|
575
696
|
const chalkInstance = await loadChalk();
|