slicejs-web-framework 2.1.0 → 2.2.1

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.
@@ -4,21 +4,350 @@ export default class Router {
4
4
  this.activeRoute = null;
5
5
  this.pathToRouteMap = this.createPathToRouteMap(routes);
6
6
 
7
- // NUEVO: Sistema de caché optimizado
7
+ // Navigation Guards
8
+ this._beforeEachGuard = null;
9
+ this._afterEachGuard = null;
10
+
11
+ // Router state
12
+ this._started = false;
13
+ this._autoStartTimeout = null;
14
+
15
+ // Sistema de caché optimizado
8
16
  this.routeContainersCache = new Map();
9
17
  this.lastCacheUpdate = 0;
10
18
  this.CACHE_DURATION = 100; // ms - caché muy corto pero efectivo
11
19
 
12
- // NUEVO: Observer para invalidar caché automáticamente
20
+ // Observer para invalidar caché automáticamente
13
21
  this.setupMutationObserver();
14
22
  }
15
23
 
16
- async init() {
17
- await this.loadInitialRoute();
24
+ /**
25
+ * Inicializa el router
26
+ * Si el usuario no llama start() manualmente, se auto-inicia después de un delay
27
+ */
28
+ init() {
18
29
  window.addEventListener('popstate', this.onRouteChange.bind(this));
30
+
31
+ // Auto-start después de 50ms si el usuario no llama start() manualmente
32
+ // Esto da tiempo para que el usuario configure guards si lo necesita
33
+ this._autoStartTimeout = setTimeout(async () => {
34
+ if (!this._started) {
35
+ slice.logger.logInfo('Router', 'Auto-starting router (no manual start() called)');
36
+ await this.start();
37
+ }
38
+ }, 50);
39
+ }
40
+
41
+ /**
42
+ * Inicia el router y carga la ruta inicial
43
+ * OPCIONAL: Solo necesario si usas guards (beforeEach/afterEach)
44
+ * Si no lo llamas, el router se auto-inicia después de 50ms
45
+ */
46
+ async start() {
47
+ // Prevenir múltiples llamadas
48
+ if (this._started) {
49
+ slice.logger.logWarning('Router', 'start() already called');
50
+ return;
51
+ }
52
+
53
+ // Cancelar auto-start si existe
54
+ if (this._autoStartTimeout) {
55
+ clearTimeout(this._autoStartTimeout);
56
+ this._autoStartTimeout = null;
57
+ }
58
+
59
+ this._started = true;
60
+ await this.loadInitialRoute();
61
+ }
62
+
63
+ // ============================================
64
+ // NAVIGATION GUARDS API
65
+ // ============================================
66
+
67
+ /**
68
+ * Registra un guard que se ejecuta ANTES de cada navegación
69
+ * Puede bloquear o redirigir la navegación
70
+ * @param {Function} guard - Función (to, from, next) => {}
71
+ */
72
+ beforeEach(guard) {
73
+ if (typeof guard !== 'function') {
74
+ slice.logger.logError('Router', 'beforeEach expects a function');
75
+ return;
76
+ }
77
+ this._beforeEachGuard = guard;
78
+ }
79
+
80
+ /**
81
+ * Registra un guard que se ejecuta DESPUÉS de cada navegación
82
+ * No puede bloquear la navegación
83
+ * @param {Function} guard - Función (to, from) => {}
84
+ */
85
+ afterEach(guard) {
86
+ if (typeof guard !== 'function') {
87
+ slice.logger.logError('Router', 'afterEach expects a function');
88
+ return;
89
+ }
90
+ this._afterEachGuard = guard;
91
+ }
92
+
93
+ /**
94
+ * Crea un objeto con información de ruta para los guards
95
+ * @param {Object} route - Objeto de ruta
96
+ * @param {Object} params - Parámetros de la ruta
97
+ * @param {String} requestedPath - Path original solicitado
98
+ * @returns {Object} Objeto con path, component, params, query, metadata
99
+ */
100
+ _createRouteInfo(route, params = {}, requestedPath = null) {
101
+ if (!route) {
102
+ return {
103
+ path: requestedPath || '/404',
104
+ component: 'NotFound',
105
+ params: {},
106
+ query: this._parseQueryParams(),
107
+ metadata: {}
108
+ };
109
+ }
110
+
111
+ return {
112
+ path: requestedPath || route.fullPath || route.path,
113
+ component: route.parentRoute ? route.parentRoute.component : route.component,
114
+ params: params,
115
+ query: this._parseQueryParams(),
116
+ metadata: route.metadata || {}
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Parsea los query parameters de la URL actual
122
+ * @returns {Object} Objeto con los query params
123
+ */
124
+ _parseQueryParams() {
125
+ const queryString = window.location.search;
126
+ if (!queryString) return {};
127
+
128
+ const params = {};
129
+ const urlParams = new URLSearchParams(queryString);
130
+
131
+ for (const [key, value] of urlParams) {
132
+ params[key] = value;
133
+ }
134
+
135
+ return params;
136
+ }
137
+
138
+ /**
139
+ * Ejecuta el beforeEach guard si existe
140
+ * @param {Object} to - Información de ruta destino
141
+ * @param {Object} from - Información de ruta origen
142
+ * @returns {String|null} Path de redirección o null si continúa
143
+ */
144
+ async _executeBeforeEachGuard(to, from) {
145
+ if (!this._beforeEachGuard) {
146
+ return null;
147
+ }
148
+
149
+ let redirectPath = null;
150
+ let nextCalled = false;
151
+
152
+ const next = (path) => {
153
+ if (nextCalled) {
154
+ slice.logger.logWarning('Router', 'next() called multiple times in guard');
155
+ return;
156
+ }
157
+ nextCalled = true;
158
+
159
+ if (path && typeof path === 'string') {
160
+ redirectPath = path;
161
+ }
162
+ };
163
+
164
+ try {
165
+ await this._beforeEachGuard(to, from, next);
166
+
167
+ // Si no se llamó next(), loguear advertencia pero continuar
168
+ if (!nextCalled) {
169
+ slice.logger.logWarning(
170
+ 'Router',
171
+ 'beforeEach guard did not call next(). Navigation will continue.'
172
+ );
173
+ }
174
+
175
+ return redirectPath;
176
+ } catch (error) {
177
+ slice.logger.logError('Router', 'Error in beforeEach guard', error);
178
+ return null; // En caso de error, continuar con la navegación
179
+ }
19
180
  }
20
181
 
21
- // NUEVO: Observer para detectar cambios en el DOM
182
+ /**
183
+ * Ejecuta el afterEach guard si existe
184
+ * @param {Object} to - Información de ruta destino
185
+ * @param {Object} from - Información de ruta origen
186
+ */
187
+ _executeAfterEachGuard(to, from) {
188
+ if (!this._afterEachGuard) {
189
+ return;
190
+ }
191
+
192
+ try {
193
+ this._afterEachGuard(to, from);
194
+ } catch (error) {
195
+ slice.logger.logError('Router', 'Error in afterEach guard', error);
196
+ }
197
+ }
198
+
199
+ // ============================================
200
+ // ROUTING CORE (MODIFICADO CON GUARDS)
201
+ // ============================================
202
+
203
+ async navigate(path, _redirectChain = []) {
204
+ const currentPath = window.location.pathname;
205
+
206
+
207
+ // Detectar loops infinitos: si ya visitamos esta ruta en la cadena de redirecciones
208
+ if (_redirectChain.includes(path)) {
209
+ slice.logger.logError(
210
+ 'Router',
211
+ `Guard redirection loop detected: ${_redirectChain.join(' → ')} → ${path}`
212
+ );
213
+ return;
214
+ }
215
+
216
+ // Límite de seguridad: máximo 10 redirecciones
217
+ if (_redirectChain.length >= 10) {
218
+ slice.logger.logError(
219
+ 'Router',
220
+ `Too many redirections: ${_redirectChain.join(' → ')} → ${path}`
221
+ );
222
+ return;
223
+ }
224
+
225
+ // Obtener información de ruta actual
226
+ const { route: fromRoute, params: fromParams } = this.matchRoute(currentPath);
227
+ const from = this._createRouteInfo(fromRoute, fromParams, currentPath);
228
+
229
+ // Obtener información de ruta destino
230
+ const { route: toRoute, params: toParams } = this.matchRoute(path);
231
+ const to = this._createRouteInfo(toRoute, toParams, path); // ← PASAR EL PATH AQUÍ
232
+
233
+
234
+ // EJECUTAR BEFORE EACH GUARD
235
+ const redirectPath = await this._executeBeforeEachGuard(to, from);
236
+
237
+ // Si el guard redirige, agregar ruta actual a la cadena y navegar a la nueva ruta
238
+ if (redirectPath) {
239
+ const newChain = [..._redirectChain, path];
240
+
241
+ return this.navigate(redirectPath, newChain);
242
+ }
243
+
244
+ // No hay redirección - continuar con la navegación normal
245
+ window.history.pushState({}, path, window.location.origin + path);
246
+ await this._performNavigation(to, from);
247
+ }
248
+
249
+ /**
250
+ * Método interno para ejecutar la navegación después de pasar los guards
251
+ * @param {Object} to - Información de ruta destino
252
+ * @param {Object} from - Información de ruta origen
253
+ */
254
+ async _performNavigation(to, from) {
255
+ // Renderizar la nueva ruta
256
+ await this.onRouteChange();
257
+
258
+ // EJECUTAR AFTER EACH GUARD
259
+ this._executeAfterEachGuard(to, from);
260
+ }
261
+
262
+ async onRouteChange() {
263
+ // Cancelar el timeout anterior si existe
264
+ if (this.routeChangeTimeout) {
265
+ clearTimeout(this.routeChangeTimeout);
266
+ }
267
+
268
+ // Debounce de 10ms para evitar múltiples llamadas seguidas
269
+ this.routeChangeTimeout = setTimeout(async () => {
270
+ const path = window.location.pathname;
271
+ const routeContainersFlag = await this.renderRoutesComponentsInPage();
272
+
273
+ if (routeContainersFlag) {
274
+ return;
275
+ }
276
+
277
+ const { route, params } = this.matchRoute(path);
278
+ if (route) {
279
+ await this.handleRoute(route, params);
280
+ }
281
+ }, 10);
282
+ }
283
+
284
+ async handleRoute(route, params) {
285
+ const targetElement = document.querySelector('#app');
286
+
287
+ const componentName = route.parentRoute ? route.parentRoute.component : route.component;
288
+ const sliceId = `route-${componentName}`;
289
+
290
+ const existingComponent = slice.controller.getComponent(sliceId);
291
+
292
+ if (slice.loading) {
293
+ slice.loading.start();
294
+ }
295
+
296
+ if (existingComponent) {
297
+ targetElement.innerHTML = '';
298
+ if (existingComponent.update) {
299
+ existingComponent.props = { ...existingComponent.props, ...params };
300
+ await existingComponent.update();
301
+ }
302
+ targetElement.appendChild(existingComponent);
303
+ await this.renderRoutesInComponent(existingComponent);
304
+ } else {
305
+ const component = await slice.build(componentName, {
306
+ params,
307
+ sliceId: sliceId,
308
+ });
309
+
310
+ targetElement.innerHTML = '';
311
+ targetElement.appendChild(component);
312
+
313
+ await this.renderRoutesInComponent(component);
314
+ }
315
+
316
+ // Invalidar caché después de cambios importantes en el DOM
317
+ this.invalidateCache();
318
+
319
+ if (slice.loading) {
320
+ slice.loading.stop();
321
+ }
322
+
323
+ slice.router.activeRoute = route;
324
+ }
325
+
326
+ async loadInitialRoute() {
327
+ const path = window.location.pathname;
328
+ const { route, params } = this.matchRoute(path);
329
+
330
+ // Para la carga inicial, también ejecutar guards
331
+ const from = this._createRouteInfo(null, {}, null);
332
+ const to = this._createRouteInfo(route, params, path); // ← PASAR EL PATH AQUÍ
333
+
334
+ // EJECUTAR BEFORE EACH GUARD en carga inicial
335
+ const redirectPath = await this._executeBeforeEachGuard(to, from);
336
+
337
+ if (redirectPath) {
338
+ return this.navigate(redirectPath);
339
+ }
340
+
341
+ await this.handleRoute(route, params);
342
+
343
+ // EJECUTAR AFTER EACH GUARD en carga inicial
344
+ this._executeAfterEachGuard(to, from);
345
+ }
346
+
347
+ // ============================================
348
+ // MÉTODOS EXISTENTES (SIN CAMBIOS)
349
+ // ============================================
350
+
22
351
  setupMutationObserver() {
23
352
  if (typeof MutationObserver !== 'undefined') {
24
353
  this.observer = new MutationObserver((mutations) => {
@@ -26,7 +355,6 @@ export default class Router {
26
355
 
27
356
  mutations.forEach((mutation) => {
28
357
  if (mutation.type === 'childList') {
29
- // Solo invalidar si se añadieron/removieron nodos que podrían ser rutas
30
358
  const addedNodes = Array.from(mutation.addedNodes);
31
359
  const removedNodes = Array.from(mutation.removedNodes);
32
360
 
@@ -55,7 +383,6 @@ export default class Router {
55
383
  }
56
384
  }
57
385
 
58
- // NUEVO: Invalidar caché
59
386
  invalidateCache() {
60
387
  this.routeContainersCache.clear();
61
388
  this.lastCacheUpdate = 0;
@@ -92,14 +419,12 @@ export default class Router {
92
419
  return pathToRouteMap;
93
420
  }
94
421
 
95
- // OPTIMIZADO: Sistema de caché inteligente
96
422
  async renderRoutesComponentsInPage(searchContainer = document) {
97
423
  let routerContainersFlag = false;
98
424
  const routeContainers = this.getCachedRouteContainers(searchContainer);
99
425
 
100
426
  for (const routeContainer of routeContainers) {
101
427
  try {
102
- // Verificar que el componente aún esté conectado al DOM
103
428
  if (!routeContainer.isConnected) {
104
429
  this.invalidateCache();
105
430
  continue;
@@ -118,18 +443,15 @@ export default class Router {
118
443
  return routerContainersFlag;
119
444
  }
120
445
 
121
- // NUEVO: Obtener contenedores con caché
122
446
  getCachedRouteContainers(container) {
123
447
  const containerKey = container === document ? 'document' : container.sliceId || 'anonymous';
124
448
  const now = Date.now();
125
449
 
126
- // Verificar si el caché es válido
127
450
  if (this.routeContainersCache.has(containerKey) &&
128
451
  (now - this.lastCacheUpdate) < this.CACHE_DURATION) {
129
452
  return this.routeContainersCache.get(containerKey);
130
453
  }
131
454
 
132
- // Regenerar caché
133
455
  const routeContainers = this.findAllRouteContainersOptimized(container);
134
456
  this.routeContainersCache.set(containerKey, routeContainers);
135
457
  this.lastCacheUpdate = now;
@@ -137,17 +459,14 @@ export default class Router {
137
459
  return routeContainers;
138
460
  }
139
461
 
140
- // OPTIMIZADO: Búsqueda más eficiente usando TreeWalker
141
462
  findAllRouteContainersOptimized(container) {
142
463
  const routeContainers = [];
143
464
 
144
- // Usar TreeWalker para una búsqueda más eficiente
145
465
  const walker = document.createTreeWalker(
146
466
  container,
147
467
  NodeFilter.SHOW_ELEMENT,
148
468
  {
149
469
  acceptNode: (node) => {
150
- // Solo aceptar nodos que sean slice-route o slice-multi-route
151
470
  if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
152
471
  return NodeFilter.FILTER_ACCEPT;
153
472
  }
@@ -164,7 +483,6 @@ export default class Router {
164
483
  return routeContainers;
165
484
  }
166
485
 
167
- // NUEVO: Método específico para renderizar rutas dentro de un componente
168
486
  async renderRoutesInComponent(component) {
169
487
  if (!component) {
170
488
  slice.logger.logWarning('Router', 'No component provided for route rendering');
@@ -174,85 +492,6 @@ export default class Router {
174
492
  return await this.renderRoutesComponentsInPage(component);
175
493
  }
176
494
 
177
- // OPTIMIZADO: Debouncing para evitar múltiples llamadas seguidas
178
- async onRouteChange() {
179
- // Cancelar el timeout anterior si existe
180
- if (this.routeChangeTimeout) {
181
- clearTimeout(this.routeChangeTimeout);
182
- }
183
-
184
- // Debounce de 10ms para evitar múltiples llamadas seguidas
185
- this.routeChangeTimeout = setTimeout(async () => {
186
- const path = window.location.pathname;
187
- const routeContainersFlag = await this.renderRoutesComponentsInPage();
188
-
189
- if (routeContainersFlag) {
190
- return;
191
- }
192
-
193
- const { route, params } = this.matchRoute(path);
194
- if (route) {
195
- await this.handleRoute(route, params);
196
- }
197
- }, 10);
198
- }
199
-
200
- async navigate(path) {
201
- window.history.pushState({}, path, window.location.origin + path);
202
- await this.onRouteChange();
203
- }
204
-
205
- async handleRoute(route, params) {
206
- const targetElement = document.querySelector('#app');
207
-
208
- const componentName = route.parentRoute ? route.parentRoute.component : route.component;
209
- const sliceId = `route-${componentName}`;
210
-
211
- const existingComponent = slice.controller.getComponent(sliceId);
212
-
213
- if (slice.loading) {
214
- slice.loading.start();
215
- }
216
-
217
- if (existingComponent) {
218
- targetElement.innerHTML = '';
219
- if (existingComponent.update) {
220
- existingComponent.props = { ...existingComponent.props, ...params };
221
- await existingComponent.update();
222
- }
223
- targetElement.appendChild(existingComponent);
224
- // Renderizar DESPUÉS de insertar (pero antes de mostrar)
225
- await this.renderRoutesInComponent(existingComponent);
226
- } else {
227
- const component = await slice.build(componentName, {
228
- params,
229
- sliceId: sliceId,
230
- });
231
-
232
- targetElement.innerHTML = '';
233
- targetElement.appendChild(component);
234
-
235
- // Renderizar INMEDIATAMENTE después de insertar
236
- await this.renderRoutesInComponent(component);
237
- }
238
-
239
- // Invalidar caché después de cambios importantes en el DOM
240
- this.invalidateCache();
241
-
242
- if (slice.loading) {
243
- slice.loading.stop();
244
- }
245
-
246
- slice.router.activeRoute = route;
247
- }
248
-
249
- async loadInitialRoute() {
250
- const path = window.location.pathname;
251
- const { route, params } = this.matchRoute(path);
252
-
253
- await this.handleRoute(route, params);
254
- }
255
-
256
495
  matchRoute(path) {
257
496
  const exactMatch = this.pathToRouteMap.get(path);
258
497
  if (exactMatch) {
package/Slice/Slice.js CHANGED
@@ -217,15 +217,6 @@ async function init() {
217
217
  window.slice.router = new RouterModule(routes);
218
218
  await window.slice.router.init();
219
219
 
220
-
221
- /*
222
- if (sliceConfig.translator.enabled) {
223
- const translator = await window.slice.build('Translator');
224
- window.slice.translator = translator;
225
- window.slice.logger.logInfo('Slice', 'Translator succesfuly enabled');
226
- }
227
- */
228
-
229
220
  }
230
221
 
231
222
  await init();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"