wu-framework 1.0.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/LICENSE +21 -0
- package/README.md +559 -0
- package/package.json +84 -0
- package/src/api/wu-simple.js +316 -0
- package/src/core/wu-app.js +192 -0
- package/src/core/wu-cache.js +374 -0
- package/src/core/wu-core.js +1296 -0
- package/src/core/wu-error-boundary.js +380 -0
- package/src/core/wu-event-bus.js +257 -0
- package/src/core/wu-hooks.js +348 -0
- package/src/core/wu-html-parser.js +280 -0
- package/src/core/wu-loader.js +271 -0
- package/src/core/wu-logger.js +119 -0
- package/src/core/wu-manifest.js +366 -0
- package/src/core/wu-performance.js +226 -0
- package/src/core/wu-plugin.js +213 -0
- package/src/core/wu-proxy-sandbox.js +153 -0
- package/src/core/wu-registry.js +130 -0
- package/src/core/wu-sandbox-pool.js +390 -0
- package/src/core/wu-sandbox.js +720 -0
- package/src/core/wu-script-executor.js +216 -0
- package/src/core/wu-snapshot-sandbox.js +184 -0
- package/src/core/wu-store.js +297 -0
- package/src/core/wu-strategies.js +241 -0
- package/src/core/wu-style-bridge.js +357 -0
- package/src/index.js +690 -0
- package/src/utils/dependency-resolver.js +326 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🪝 WU-HOOKS: LIFECYCLE MIDDLEWARE SYSTEM
|
|
3
|
+
*
|
|
4
|
+
* Sistema de hooks basado en middleware pattern para control fino:
|
|
5
|
+
* - Middleware chain con next()
|
|
6
|
+
* - Puede cancelar operaciones (no llamar next)
|
|
7
|
+
* - Puede modificar contexto
|
|
8
|
+
* - Prioridad de hooks
|
|
9
|
+
* - Async/await support
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class WuLifecycleHooks {
|
|
13
|
+
constructor(core) {
|
|
14
|
+
this.core = core;
|
|
15
|
+
this.hooks = new Map();
|
|
16
|
+
this.executionLog = [];
|
|
17
|
+
this.maxLogSize = 100;
|
|
18
|
+
|
|
19
|
+
// Lifecycle phases disponibles
|
|
20
|
+
this.lifecyclePhases = [
|
|
21
|
+
'beforeInit', // Antes de inicializar framework
|
|
22
|
+
'afterInit', // Después de inicializar
|
|
23
|
+
'beforeLoad', // Antes de cargar una app
|
|
24
|
+
'afterLoad', // Después de cargar
|
|
25
|
+
'beforeMount', // Antes de montar
|
|
26
|
+
'afterMount', // Después de montar
|
|
27
|
+
'beforeUnmount', // Antes de desmontar
|
|
28
|
+
'afterUnmount', // Después de desmontar
|
|
29
|
+
'beforeDestroy', // Antes de destruir framework
|
|
30
|
+
'afterDestroy' // Después de destruir
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Inicializar hooks
|
|
34
|
+
this.lifecyclePhases.forEach(phase => {
|
|
35
|
+
this.hooks.set(phase, []);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log('[WuHooks] 🪝 Lifecycle hooks initialized');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 📦 USE: Registrar middleware hook
|
|
43
|
+
* @param {string} phase - Fase del lifecycle
|
|
44
|
+
* @param {Function} middleware - Función middleware (context, next)
|
|
45
|
+
* @param {Object} options - { priority, name }
|
|
46
|
+
*/
|
|
47
|
+
use(phase, middleware, options = {}) {
|
|
48
|
+
if (!this.hooks.has(phase)) {
|
|
49
|
+
throw new Error(`[WuHooks] Unknown lifecycle phase: ${phase}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof middleware !== 'function') {
|
|
53
|
+
throw new Error('[WuHooks] Middleware must be a function');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hook = {
|
|
57
|
+
middleware,
|
|
58
|
+
name: options.name || `hook_${Date.now()}`,
|
|
59
|
+
priority: options.priority || 0,
|
|
60
|
+
registeredAt: Date.now()
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const hooks = this.hooks.get(phase);
|
|
64
|
+
hooks.push(hook);
|
|
65
|
+
|
|
66
|
+
// Ordenar por prioridad (mayor primero)
|
|
67
|
+
hooks.sort((a, b) => b.priority - a.priority);
|
|
68
|
+
|
|
69
|
+
console.log(`[WuHooks] Hook "${hook.name}" registered for ${phase} (priority: ${hook.priority})`);
|
|
70
|
+
|
|
71
|
+
// Retornar función para desregistrar
|
|
72
|
+
return () => this.remove(phase, hook.name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 🗑️ REMOVE: Remover hook
|
|
77
|
+
* @param {string} phase - Fase del lifecycle
|
|
78
|
+
* @param {string} name - Nombre del hook
|
|
79
|
+
*/
|
|
80
|
+
remove(phase, name) {
|
|
81
|
+
if (!this.hooks.has(phase)) return;
|
|
82
|
+
|
|
83
|
+
const hooks = this.hooks.get(phase);
|
|
84
|
+
const index = hooks.findIndex(h => h.name === name);
|
|
85
|
+
|
|
86
|
+
if (index > -1) {
|
|
87
|
+
hooks.splice(index, 1);
|
|
88
|
+
console.log(`[WuHooks] Hook "${name}" removed from ${phase}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 🎯 EXECUTE: Ejecutar middleware chain
|
|
94
|
+
* @param {string} phase - Fase del lifecycle
|
|
95
|
+
* @param {Object} context - Contexto a pasar
|
|
96
|
+
* @returns {Promise<Object>} Contexto modificado o { cancelled: true }
|
|
97
|
+
*/
|
|
98
|
+
async execute(phase, context = {}) {
|
|
99
|
+
const hooks = this.hooks.get(phase);
|
|
100
|
+
|
|
101
|
+
if (!hooks || hooks.length === 0) {
|
|
102
|
+
return context;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`[WuHooks] Executing ${hooks.length} hooks for ${phase}`);
|
|
106
|
+
|
|
107
|
+
// Log para debugging
|
|
108
|
+
const executionEntry = {
|
|
109
|
+
phase,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
hooksCount: hooks.length,
|
|
112
|
+
hookNames: hooks.map(h => h.name)
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
let currentContext = { ...context };
|
|
116
|
+
let cancelled = false;
|
|
117
|
+
|
|
118
|
+
// Crear cadena de middleware
|
|
119
|
+
const executeChain = async (index) => {
|
|
120
|
+
// Si llegamos al final de la cadena, retornar contexto
|
|
121
|
+
if (index >= hooks.length) {
|
|
122
|
+
return currentContext;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const hook = hooks[index];
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
let nextCalled = false;
|
|
130
|
+
|
|
131
|
+
// Función next
|
|
132
|
+
const next = async (modifiedContext) => {
|
|
133
|
+
nextCalled = true;
|
|
134
|
+
|
|
135
|
+
// Si se pasa un contexto modificado, usarlo
|
|
136
|
+
if (modifiedContext !== undefined) {
|
|
137
|
+
currentContext = { ...currentContext, ...modifiedContext };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Continuar con siguiente hook
|
|
141
|
+
return await executeChain(index + 1);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Ejecutar middleware
|
|
145
|
+
await hook.middleware(currentContext, next);
|
|
146
|
+
|
|
147
|
+
// Si no se llamó next(), la operación fue cancelada
|
|
148
|
+
if (!nextCalled) {
|
|
149
|
+
console.log(`[WuHooks] Hook "${hook.name}" cancelled execution`);
|
|
150
|
+
cancelled = true;
|
|
151
|
+
return { cancelled: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const duration = Date.now() - startTime;
|
|
155
|
+
console.log(`[WuHooks] Hook "${hook.name}" executed in ${duration}ms`);
|
|
156
|
+
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`[WuHooks] Error in hook "${hook.name}":`, error);
|
|
159
|
+
|
|
160
|
+
// Si hay error, pasar al siguiente hook
|
|
161
|
+
return await executeChain(index + 1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return currentContext;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Ejecutar cadena
|
|
168
|
+
const result = await executeChain(0);
|
|
169
|
+
|
|
170
|
+
// Completar log
|
|
171
|
+
executionEntry.duration = Date.now() - executionEntry.timestamp;
|
|
172
|
+
executionEntry.cancelled = cancelled;
|
|
173
|
+
executionEntry.success = !cancelled;
|
|
174
|
+
|
|
175
|
+
this.executionLog.push(executionEntry);
|
|
176
|
+
|
|
177
|
+
// Mantener límite de log
|
|
178
|
+
if (this.executionLog.length > this.maxLogSize) {
|
|
179
|
+
this.executionLog.shift();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 🚀 HELPER: Registrar hook para múltiples fases
|
|
187
|
+
* @param {Array<string>} phases - Fases del lifecycle
|
|
188
|
+
* @param {Function} middleware - Función middleware
|
|
189
|
+
* @param {Object} options - Opciones
|
|
190
|
+
* @returns {Function} Función para desregistrar de todas las fases
|
|
191
|
+
*/
|
|
192
|
+
useMultiple(phases, middleware, options = {}) {
|
|
193
|
+
const unregisterFns = phases.map(phase =>
|
|
194
|
+
this.use(phase, middleware, { ...options, name: `${options.name}_${phase}` })
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Retornar función que desregistra de todas las fases
|
|
198
|
+
return () => unregisterFns.forEach(fn => fn());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 📋 GET HOOKS: Obtener hooks registrados
|
|
203
|
+
* @param {string} phase - Fase del lifecycle (opcional)
|
|
204
|
+
* @returns {Object|Array}
|
|
205
|
+
*/
|
|
206
|
+
getHooks(phase) {
|
|
207
|
+
if (phase) {
|
|
208
|
+
return this.hooks.get(phase) || [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Retornar todos los hooks
|
|
212
|
+
const allHooks = {};
|
|
213
|
+
this.hooks.forEach((hooks, phase) => {
|
|
214
|
+
allHooks[phase] = hooks.map(h => ({
|
|
215
|
+
name: h.name,
|
|
216
|
+
priority: h.priority,
|
|
217
|
+
registeredAt: h.registeredAt
|
|
218
|
+
}));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return allHooks;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 📊 GET STATS: Estadísticas de hooks
|
|
226
|
+
* @returns {Object}
|
|
227
|
+
*/
|
|
228
|
+
getStats() {
|
|
229
|
+
const totalHooks = Array.from(this.hooks.values())
|
|
230
|
+
.reduce((sum, hooks) => sum + hooks.length, 0);
|
|
231
|
+
|
|
232
|
+
const executionsByPhase = {};
|
|
233
|
+
this.executionLog.forEach(entry => {
|
|
234
|
+
executionsByPhase[entry.phase] = (executionsByPhase[entry.phase] || 0) + 1;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const avgDuration = this.executionLog.length > 0
|
|
238
|
+
? this.executionLog.reduce((sum, entry) => sum + entry.duration, 0) / this.executionLog.length
|
|
239
|
+
: 0;
|
|
240
|
+
|
|
241
|
+
const cancelledCount = this.executionLog.filter(entry => entry.cancelled).length;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
totalHooks,
|
|
245
|
+
totalExecutions: this.executionLog.length,
|
|
246
|
+
executionsByPhase,
|
|
247
|
+
avgDuration: Math.round(avgDuration),
|
|
248
|
+
cancelledCount,
|
|
249
|
+
recentExecutions: this.executionLog.slice(-10)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 🧹 CLEANUP: Limpiar todos los hooks
|
|
255
|
+
* @param {string} phase - Fase específica (opcional)
|
|
256
|
+
*/
|
|
257
|
+
cleanup(phase) {
|
|
258
|
+
if (phase) {
|
|
259
|
+
this.hooks.set(phase, []);
|
|
260
|
+
console.log(`[WuHooks] Hooks cleaned for ${phase}`);
|
|
261
|
+
} else {
|
|
262
|
+
this.lifecyclePhases.forEach(p => {
|
|
263
|
+
this.hooks.set(p, []);
|
|
264
|
+
});
|
|
265
|
+
this.executionLog = [];
|
|
266
|
+
console.log('[WuHooks] 🧹 All hooks cleaned');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 🔧 HELPER: Crear middleware hooks fácilmente
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Crear hook simple que siempre llama next
|
|
277
|
+
* @param {Function} fn - Función a ejecutar
|
|
278
|
+
* @returns {Function} Middleware function
|
|
279
|
+
*/
|
|
280
|
+
export const createSimpleHook = (fn) => {
|
|
281
|
+
return async (context, next) => {
|
|
282
|
+
await fn(context);
|
|
283
|
+
await next();
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Crear hook condicional
|
|
289
|
+
* @param {Function} condition - Función de condición (context) => boolean
|
|
290
|
+
* @param {Function} fn - Función a ejecutar si condición es true
|
|
291
|
+
* @returns {Function} Middleware function
|
|
292
|
+
*/
|
|
293
|
+
export const createConditionalHook = (condition, fn) => {
|
|
294
|
+
return async (context, next) => {
|
|
295
|
+
if (await condition(context)) {
|
|
296
|
+
await fn(context);
|
|
297
|
+
}
|
|
298
|
+
await next();
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Crear hook que puede cancelar operación
|
|
304
|
+
* @param {Function} shouldContinue - Función que retorna true para continuar
|
|
305
|
+
* @returns {Function} Middleware function
|
|
306
|
+
*/
|
|
307
|
+
export const createGuardHook = (shouldContinue) => {
|
|
308
|
+
return async (context, next) => {
|
|
309
|
+
if (await shouldContinue(context)) {
|
|
310
|
+
await next();
|
|
311
|
+
}
|
|
312
|
+
// Si no retorna true, no llama next() y cancela
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Crear hook que modifica contexto
|
|
318
|
+
* @param {Function} transformer - Función que transforma el contexto
|
|
319
|
+
* @returns {Function} Middleware function
|
|
320
|
+
*/
|
|
321
|
+
export const createTransformHook = (transformer) => {
|
|
322
|
+
return async (context, next) => {
|
|
323
|
+
const modified = await transformer(context);
|
|
324
|
+
await next(modified);
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Crear hook con timeout
|
|
330
|
+
* @param {Function} fn - Función a ejecutar
|
|
331
|
+
* @param {number} timeout - Timeout en ms
|
|
332
|
+
* @returns {Function} Middleware function
|
|
333
|
+
*/
|
|
334
|
+
export const createTimedHook = (fn, timeout = 5000) => {
|
|
335
|
+
return async (context, next) => {
|
|
336
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
337
|
+
setTimeout(() => reject(new Error('Hook timeout')), timeout)
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
await Promise.race([fn(context), timeoutPromise]);
|
|
342
|
+
await next();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('[WuHooks] Timed hook failed:', error);
|
|
345
|
+
await next(); // Continuar a pesar del error
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 📄 WU-HTML-PARSER: Parser inteligente de HTML para micro-apps
|
|
3
|
+
* Basado en video-code - Extrae DOM, scripts y estilos
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class WuHtmlParser {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.cache = new Map(); // Cache de HTML parseado
|
|
9
|
+
console.log('[WuHtmlParser] 📄 HTML parsing system initialized');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parsear HTML completo de una micro-app
|
|
14
|
+
* @param {string} html - HTML a parsear
|
|
15
|
+
* @param {string} appName - Nombre de la app
|
|
16
|
+
* @param {string} baseUrl - URL base para resolver recursos
|
|
17
|
+
* @returns {Object} { dom, scripts, styles, externalScripts, externalStyles }
|
|
18
|
+
*/
|
|
19
|
+
parse(html, appName, baseUrl) {
|
|
20
|
+
console.log(`[WuHtmlParser] 📄 Parsing HTML for ${appName}`);
|
|
21
|
+
|
|
22
|
+
// Verificar cache
|
|
23
|
+
const cacheKey = `${appName}:${html.length}`;
|
|
24
|
+
if (this.cache.has(cacheKey)) {
|
|
25
|
+
console.log(`[WuHtmlParser] ⚡ Cache hit for ${appName}`);
|
|
26
|
+
return this.cache.get(cacheKey);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Crear contenedor temporal para parsear
|
|
31
|
+
const tempDiv = document.createElement('div');
|
|
32
|
+
tempDiv.innerHTML = html;
|
|
33
|
+
|
|
34
|
+
// Extraer recursos
|
|
35
|
+
const inlineScripts = [];
|
|
36
|
+
const externalScripts = [];
|
|
37
|
+
const inlineStyles = [];
|
|
38
|
+
const externalStyles = [];
|
|
39
|
+
|
|
40
|
+
// 🔍 Parsear recursivamente el DOM
|
|
41
|
+
this.deepParse(tempDiv, {
|
|
42
|
+
inlineScripts,
|
|
43
|
+
externalScripts,
|
|
44
|
+
inlineStyles,
|
|
45
|
+
externalStyles,
|
|
46
|
+
baseUrl,
|
|
47
|
+
appName
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Obtener DOM limpio
|
|
51
|
+
const cleanedDom = tempDiv.innerHTML;
|
|
52
|
+
|
|
53
|
+
const result = {
|
|
54
|
+
dom: cleanedDom,
|
|
55
|
+
scripts: {
|
|
56
|
+
inline: inlineScripts,
|
|
57
|
+
external: externalScripts,
|
|
58
|
+
all: [...inlineScripts, ...externalScripts]
|
|
59
|
+
},
|
|
60
|
+
styles: {
|
|
61
|
+
inline: inlineStyles,
|
|
62
|
+
external: externalStyles,
|
|
63
|
+
all: [...inlineStyles, ...externalStyles]
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Cachear resultado
|
|
68
|
+
this.cache.set(cacheKey, result);
|
|
69
|
+
|
|
70
|
+
console.log(`[WuHtmlParser] ✅ Parsed ${appName}: ${inlineScripts.length} inline scripts, ${externalScripts.length} external scripts`);
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`[WuHtmlParser] ❌ Failed to parse HTML for ${appName}:`, error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parsear recursivamente el DOM extrayendo scripts y estilos
|
|
82
|
+
* @param {HTMLElement} element - Elemento a parsear
|
|
83
|
+
* @param {Object} context - Contexto de parsing
|
|
84
|
+
*/
|
|
85
|
+
deepParse(element, context) {
|
|
86
|
+
const { inlineScripts, externalScripts, inlineStyles, externalStyles, baseUrl, appName } = context;
|
|
87
|
+
const children = Array.from(element.children);
|
|
88
|
+
|
|
89
|
+
for (const child of children) {
|
|
90
|
+
const tagName = child.nodeName.toLowerCase();
|
|
91
|
+
|
|
92
|
+
// 📜 SCRIPT TAGS
|
|
93
|
+
if (tagName === 'script') {
|
|
94
|
+
this.parseScriptTag(child, inlineScripts, externalScripts, baseUrl, appName);
|
|
95
|
+
|
|
96
|
+
// Reemplazar script con comentario
|
|
97
|
+
const comment = document.createComment(`Wu: script removed from ${appName}`);
|
|
98
|
+
child.parentElement?.replaceChild(comment, child);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 🎨 STYLE TAGS
|
|
103
|
+
if (tagName === 'style') {
|
|
104
|
+
const styleContent = child.textContent || '';
|
|
105
|
+
if (styleContent.trim()) {
|
|
106
|
+
inlineStyles.push(styleContent);
|
|
107
|
+
console.log(`[WuHtmlParser] 🎨 Extracted inline style (${styleContent.length} chars)`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Reemplazar style con comentario
|
|
111
|
+
const comment = document.createComment(`Wu: style removed from ${appName}`);
|
|
112
|
+
child.parentElement?.replaceChild(comment, child);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 🔗 LINK TAGS (external styles)
|
|
117
|
+
if (tagName === 'link') {
|
|
118
|
+
const rel = child.getAttribute('rel');
|
|
119
|
+
const href = child.getAttribute('href');
|
|
120
|
+
|
|
121
|
+
if (rel === 'stylesheet' && href) {
|
|
122
|
+
const absoluteUrl = this.resolveUrl(href, baseUrl);
|
|
123
|
+
externalStyles.push(absoluteUrl);
|
|
124
|
+
console.log(`[WuHtmlParser] 🔗 Extracted external style: ${absoluteUrl}`);
|
|
125
|
+
|
|
126
|
+
// Reemplazar link con comentario
|
|
127
|
+
const comment = document.createComment(`Wu: link removed from ${appName}`);
|
|
128
|
+
child.parentElement?.replaceChild(comment, child);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Recursión para elementos hijos
|
|
134
|
+
if (child.children.length > 0) {
|
|
135
|
+
this.deepParse(child, context);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parsear tag <script>
|
|
142
|
+
*/
|
|
143
|
+
parseScriptTag(scriptElement, inlineScripts, externalScripts, baseUrl, appName) {
|
|
144
|
+
const src = scriptElement.getAttribute('src');
|
|
145
|
+
const type = scriptElement.getAttribute('type') || 'text/javascript';
|
|
146
|
+
|
|
147
|
+
// Ignorar module scripts (se cargan por import)
|
|
148
|
+
if (type === 'module') {
|
|
149
|
+
console.log(`[WuHtmlParser] ⏭️ Skipping module script for ${appName}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (src) {
|
|
154
|
+
// Script externo
|
|
155
|
+
const absoluteUrl = this.resolveUrl(src, baseUrl);
|
|
156
|
+
externalScripts.push(absoluteUrl);
|
|
157
|
+
console.log(`[WuHtmlParser] 📜 Extracted external script: ${absoluteUrl}`);
|
|
158
|
+
} else {
|
|
159
|
+
// Script inline
|
|
160
|
+
const scriptContent = scriptElement.textContent || '';
|
|
161
|
+
if (scriptContent.trim()) {
|
|
162
|
+
inlineScripts.push(scriptContent);
|
|
163
|
+
console.log(`[WuHtmlParser] 📜 Extracted inline script (${scriptContent.length} chars)`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolver URL relativa a absoluta
|
|
170
|
+
*/
|
|
171
|
+
resolveUrl(url, baseUrl) {
|
|
172
|
+
// Ya es absoluta
|
|
173
|
+
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
|
|
174
|
+
return url.startsWith('//') ? `https:${url}` : url;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Resolver relativa
|
|
178
|
+
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
179
|
+
|
|
180
|
+
if (url.startsWith('/')) {
|
|
181
|
+
// Relativa a raíz
|
|
182
|
+
const urlObj = new URL(baseUrl);
|
|
183
|
+
return `${urlObj.protocol}//${urlObj.host}${url}`;
|
|
184
|
+
} else {
|
|
185
|
+
// Relativa a base
|
|
186
|
+
return `${base}${url}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Cargar HTML desde URL
|
|
192
|
+
*/
|
|
193
|
+
async fetchHtml(url, appName) {
|
|
194
|
+
console.log(`[WuHtmlParser] 🌐 Fetching HTML for ${appName} from ${url}`);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch(url, {
|
|
198
|
+
method: 'GET',
|
|
199
|
+
cache: 'no-cache',
|
|
200
|
+
headers: {
|
|
201
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const html = await response.text();
|
|
210
|
+
|
|
211
|
+
if (!html || html.trim().length === 0) {
|
|
212
|
+
throw new Error('Empty HTML response');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`[WuHtmlParser] ✅ Fetched HTML (${html.length} chars)`);
|
|
216
|
+
|
|
217
|
+
return html;
|
|
218
|
+
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(`[WuHtmlParser] ❌ Failed to fetch HTML from ${url}:`, error);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parsear y cargar HTML desde URL
|
|
227
|
+
*/
|
|
228
|
+
async parseFromUrl(url, appName) {
|
|
229
|
+
const html = await this.fetchHtml(url, appName);
|
|
230
|
+
return this.parse(html, appName, url);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Limpiar cache
|
|
235
|
+
*/
|
|
236
|
+
clearCache(pattern) {
|
|
237
|
+
if (pattern) {
|
|
238
|
+
const regex = new RegExp(pattern);
|
|
239
|
+
for (const [key] of this.cache) {
|
|
240
|
+
if (regex.test(key)) {
|
|
241
|
+
this.cache.delete(key);
|
|
242
|
+
console.log(`[WuHtmlParser] 🗑️ Cleared cache for: ${key}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
this.cache.clear();
|
|
247
|
+
console.log(`[WuHtmlParser] 🗑️ Cache cleared completely`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Obtener estadísticas
|
|
253
|
+
*/
|
|
254
|
+
getStats() {
|
|
255
|
+
return {
|
|
256
|
+
cacheSize: this.cache.size,
|
|
257
|
+
cacheKeys: Array.from(this.cache.keys())
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 🎯 EXPORTS DE CONVENIENCIA
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parsear HTML
|
|
268
|
+
*/
|
|
269
|
+
export function parseHtml(html, appName, baseUrl) {
|
|
270
|
+
const parser = new WuHtmlParser();
|
|
271
|
+
return parser.parse(html, appName, baseUrl);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Parsear HTML desde URL
|
|
276
|
+
*/
|
|
277
|
+
export async function parseHtmlFromUrl(url, appName) {
|
|
278
|
+
const parser = new WuHtmlParser();
|
|
279
|
+
return await parser.parseFromUrl(url, appName);
|
|
280
|
+
}
|