sibujs 3.4.0 → 4.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.
Files changed (98) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +118 -15
  3. package/dist/browser.cjs +410 -101
  4. package/dist/browser.d.cts +50 -5
  5. package/dist/browser.d.ts +50 -5
  6. package/dist/browser.js +8 -6
  7. package/dist/build.cjs +777 -385
  8. package/dist/build.js +15 -15
  9. package/dist/cdn.global.js +11 -7
  10. package/dist/{chunk-JCO7AHNU.js → chunk-3EKJK5FZ.js} +73 -34
  11. package/dist/chunk-4MFZMLUU.js +147 -0
  12. package/dist/chunk-52XFPGSN.js +123 -0
  13. package/dist/chunk-5INI7D2L.js +194 -0
  14. package/dist/{chunk-OAVXAFAY.js → chunk-6LG2T7LO.js} +5 -5
  15. package/dist/{chunk-OOUQUCNN.js → chunk-ACBT6UZS.js} +57 -22
  16. package/dist/{chunk-F7HW2NI5.js → chunk-AW6HPJ3L.js} +1 -1
  17. package/dist/chunk-DHDZ7IVN.js +109 -0
  18. package/dist/{chunk-JNGGM2XE.js → chunk-DQVUBVNX.js} +16 -25
  19. package/dist/{chunk-AXFWGY32.js → chunk-DR7H6UGM.js} +232 -67
  20. package/dist/{chunk-WSGLJYFI.js → chunk-E62UADKL.js} +9 -3
  21. package/dist/{chunk-3Z2NGWQI.js → chunk-ES2GERY2.js} +1 -1
  22. package/dist/{chunk-S3V6ZXPI.js → chunk-EWEE7NBF.js} +115 -41
  23. package/dist/{chunk-3DVGCW7O.js → chunk-F64ZPCFE.js} +8 -8
  24. package/dist/{chunk-APFRCIRI.js → chunk-GCSGGA3Q.js} +295 -198
  25. package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
  26. package/dist/{chunk-ELXMRSVN.js → chunk-NF3LYCQH.js} +259 -124
  27. package/dist/chunk-O3QQ3XOL.js +61 -0
  28. package/dist/{chunk-BUKYCWHX.js → chunk-PBQEW5VW.js} +14 -10
  29. package/dist/{chunk-H5FI5WDP.js → chunk-RRIDJQ5C.js} +3 -3
  30. package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
  31. package/dist/{chunk-ODXVQ3CS.js → chunk-VFHIVEPF.js} +309 -188
  32. package/dist/chunk-VPP2FONR.js +142 -0
  33. package/dist/{chunk-KZBEVFXZ.js → chunk-W55YHPEP.js} +5 -3
  34. package/dist/{chunk-FKQG7FTY.js → chunk-WN6WDFCB.js} +4 -4
  35. package/dist/{chunk-CEBWDD63.js → chunk-WOOMUJFT.js} +228 -86
  36. package/dist/{chunk-ITWCTZYH.js → chunk-Z2CE42DB.js} +19 -25
  37. package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.cts} +6 -6
  38. package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.ts} +6 -6
  39. package/dist/{customElement-CPfIrbvg.d.ts → customElement-CKLoO63w.d.cts} +73 -11
  40. package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.ts} +73 -11
  41. package/dist/data.cjs +483 -205
  42. package/dist/data.d.cts +139 -7
  43. package/dist/data.d.ts +139 -7
  44. package/dist/data.js +9 -9
  45. package/dist/devtools.cjs +222 -87
  46. package/dist/devtools.js +8 -8
  47. package/dist/dispose-4FZ6OFXR.js +19 -0
  48. package/dist/ecosystem.cjs +419 -175
  49. package/dist/ecosystem.js +11 -11
  50. package/dist/extras.cjs +1981 -765
  51. package/dist/extras.d.cts +7 -7
  52. package/dist/extras.d.ts +7 -7
  53. package/dist/extras.js +29 -26
  54. package/dist/index.cjs +784 -385
  55. package/dist/index.d.cts +299 -28
  56. package/dist/index.d.ts +299 -28
  57. package/dist/index.js +33 -18
  58. package/dist/motion.cjs +180 -50
  59. package/dist/motion.d.cts +26 -0
  60. package/dist/motion.d.ts +26 -0
  61. package/dist/motion.js +5 -5
  62. package/dist/patterns.cjs +437 -170
  63. package/dist/patterns.d.cts +60 -13
  64. package/dist/patterns.d.ts +60 -13
  65. package/dist/patterns.js +7 -7
  66. package/dist/performance.cjs +333 -148
  67. package/dist/performance.d.cts +55 -7
  68. package/dist/performance.d.ts +55 -7
  69. package/dist/performance.js +9 -9
  70. package/dist/plugins.cjs +1374 -576
  71. package/dist/plugins.d.cts +227 -9
  72. package/dist/plugins.d.ts +227 -9
  73. package/dist/plugins.js +613 -321
  74. package/dist/{ssr-D62yFwuw.d.cts → ssr-BiPRdZ6n.d.cts} +44 -15
  75. package/dist/{ssr-D62yFwuw.d.ts → ssr-BiPRdZ6n.d.ts} +44 -15
  76. package/dist/{ssr-2PPULEK2.js → ssr-JFP5535P.js} +5 -4
  77. package/dist/ssr.cjs +1104 -443
  78. package/dist/ssr.d.cts +230 -20
  79. package/dist/ssr.d.ts +230 -20
  80. package/dist/ssr.js +15 -12
  81. package/dist/{startup-0Qv6aosO.d.cts → startup-BMpaiMhP.d.cts} +1 -1
  82. package/dist/{startup-0Qv6aosO.d.ts → startup-BMpaiMhP.d.ts} +1 -1
  83. package/dist/testing.cjs +205 -63
  84. package/dist/testing.d.cts +1 -1
  85. package/dist/testing.d.ts +1 -1
  86. package/dist/testing.js +8 -10
  87. package/dist/ui.cjs +640 -199
  88. package/dist/ui.d.cts +2 -2
  89. package/dist/ui.d.ts +2 -2
  90. package/dist/ui.js +10 -10
  91. package/dist/widgets.cjs +207 -82
  92. package/dist/widgets.js +9 -9
  93. package/package.json +12 -7
  94. package/dist/chunk-5VGSK6D2.js +0 -80
  95. package/dist/chunk-COY6PUD2.js +0 -34
  96. package/dist/chunk-K35BAR4P.js +0 -96
  97. package/dist/chunk-L3GAGWCC.js +0 -111
  98. package/dist/chunk-RT3IYFN2.js +0 -38
package/dist/plugins.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  preloadCritical,
19
19
  prerenderRoutes,
20
20
  satisfies
21
- } from "./chunk-F7HW2NI5.js";
21
+ } from "./chunk-AW6HPJ3L.js";
22
22
  import {
23
23
  isUnsafeKey
24
24
  } from "./chunk-H3SRKIYX.js";
@@ -35,78 +35,126 @@ import {
35
35
  } from "./chunk-WWV3SJ3L.js";
36
36
  import {
37
37
  span
38
- } from "./chunk-3Z2NGWQI.js";
38
+ } from "./chunk-ES2GERY2.js";
39
39
  import {
40
40
  escapeScriptJson,
41
- isDangerousMetaRefresh,
42
- renderToString
43
- } from "./chunk-EX77FXTT.js";
44
- import "./chunk-JNGGM2XE.js";
45
- import "./chunk-K35BAR4P.js";
46
- import {
47
- globalSingleton
48
- } from "./chunk-7ZHH77QA.js";
41
+ renderToString,
42
+ serializeHeadEntry
43
+ } from "./chunk-HYCCIYNS.js";
44
+ import "./chunk-DQVUBVNX.js";
45
+ import "./chunk-4MFZMLUU.js";
49
46
  import {
50
47
  isUrlAttribute,
51
- sanitizeCSSValue,
48
+ sanitizeStyleAttribute,
52
49
  sanitizeUrl,
53
50
  stripControlChars
54
- } from "./chunk-L3GAGWCC.js";
51
+ } from "./chunk-5INI7D2L.js";
52
+ import {
53
+ globalSingleton
54
+ } from "./chunk-7ZHH77QA.js";
55
55
  import {
56
56
  effect
57
- } from "./chunk-BUKYCWHX.js";
58
- import "./chunk-S3NFJO6L.js";
57
+ } from "./chunk-PBQEW5VW.js";
58
+ import {
59
+ getRequestStore
60
+ } from "./chunk-TIRZCERI.js";
59
61
  import {
60
62
  dispose,
61
63
  registerDisposer
62
- } from "./chunk-5VGSK6D2.js";
64
+ } from "./chunk-DHDZ7IVN.js";
63
65
  import {
64
66
  signal
65
- } from "./chunk-KZBEVFXZ.js";
67
+ } from "./chunk-W55YHPEP.js";
66
68
  import {
67
69
  track
68
- } from "./chunk-OOUQUCNN.js";
69
- import "./chunk-COY6PUD2.js";
70
+ } from "./chunk-ACBT6UZS.js";
71
+ import {
72
+ devWarn,
73
+ isDev
74
+ } from "./chunk-VPP2FONR.js";
70
75
 
71
76
  // src/plugins/i18n.ts
72
77
  var _i18n = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.i18n.v1"), () => ({
73
78
  locale: signal("en"),
74
- locales: {}
79
+ locales: /* @__PURE__ */ Object.create(null)
75
80
  }));
76
- var [currentLocale, setLocaleInternal] = _i18n.locale;
81
+ var [clientLocale, setClientLocale] = _i18n.locale;
77
82
  var locales = _i18n.locales;
83
+ function dictionaryFor(locale) {
84
+ return Object.hasOwn(locales, locale) ? locales[locale] : void 0;
85
+ }
86
+ function lookup(key) {
87
+ const dictionary = dictionaryFor(getLocale());
88
+ if (dictionary === void 0 || !Object.hasOwn(dictionary, key)) return void 0;
89
+ const message = dictionary[key];
90
+ return typeof message === "string" ? message : void 0;
91
+ }
78
92
  function setLocale(locale) {
79
- setLocaleInternal(locale);
93
+ const request = getRequestStore();
94
+ if (request) {
95
+ request.locale = locale;
96
+ return;
97
+ }
98
+ setClientLocale(locale);
80
99
  }
81
100
  function getLocale() {
82
- return currentLocale();
101
+ const request = getRequestStore();
102
+ if (request) {
103
+ return request.locale ?? clientLocale();
104
+ }
105
+ return clientLocale();
83
106
  }
84
107
  function registerTranslations(locale, messages) {
85
- locales[locale] = { ...locales[locale], ...messages };
108
+ const prepared = { ...messages };
109
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), dictionaryFor(locale), prepared);
110
+ Object.defineProperty(locales, locale, {
111
+ value: merged,
112
+ writable: true,
113
+ enumerable: true,
114
+ configurable: true
115
+ });
86
116
  }
87
117
  function t(key, params) {
88
- const locale = currentLocale();
89
- const message = locales[locale]?.[key] || key;
118
+ const message = lookup(key) ?? key;
90
119
  return params ? message.replace(/\{(\w+)\}/g, (_, p) => String(params[p] ?? "")) : message;
91
120
  }
92
121
  function Trans(key, params) {
93
122
  return span(() => t(key, params));
94
123
  }
95
124
  function hasTranslation(key) {
96
- const locale = currentLocale();
97
- return key in (locales[locale] || {});
125
+ return lookup(key) !== void 0;
98
126
  }
99
127
  function getAvailableLocales() {
100
128
  return Object.keys(locales);
101
129
  }
102
130
 
103
131
  // src/plugins/router.ts
104
- function isSafeNavigationTarget(path) {
105
- if (path === "") return true;
132
+ function classifyNavigationTarget(path) {
133
+ if (path === "") return "internal";
106
134
  const normalized = stripControlChars(path).replace(/\\/g, "/");
107
- if (normalized.startsWith("//")) return false;
108
- return sanitizeUrl(normalized) !== "";
135
+ if (normalized === "") return "internal";
136
+ if (normalized.startsWith("//")) return "unsafe";
137
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(normalized)) return "internal";
138
+ return sanitizeUrl(normalized) === "" ? "unsafe" : "external";
139
+ }
140
+ function isRouterNavigable(path) {
141
+ return classifyNavigationTarget(path) === "internal";
142
+ }
143
+ function normalizePathname(path) {
144
+ if (!path) return "/";
145
+ const trimmed = path.length > 1 ? path.replace(/\/+$/, "") : path;
146
+ return trimmed || "/";
109
147
  }
148
+ function normalizeQuery(query) {
149
+ const params = new URLSearchParams(query);
150
+ return [...params.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${k}=${v}`).join("&");
151
+ }
152
+ function isPathnameAncestor(target, current) {
153
+ if (target === current) return true;
154
+ if (target === "/") return false;
155
+ return current.startsWith(`${target}/`);
156
+ }
157
+ var ABORT_ROUTER_DESTROYED = "sibu:router-destroyed";
110
158
  var LRUCache = class {
111
159
  constructor(maxSize = 100) {
112
160
  this.cache = /* @__PURE__ */ new Map();
@@ -167,9 +215,9 @@ var NavigationController = class {
167
215
  }
168
216
  }
169
217
  }
170
- abort() {
218
+ abort(reason) {
171
219
  if (this.currentController) {
172
- this.currentController.abort();
220
+ this.currentController.abort(reason);
173
221
  this.currentController = null;
174
222
  }
175
223
  }
@@ -355,6 +403,7 @@ var GuardManager = class {
355
403
  this.timeout = timeout;
356
404
  }
357
405
  async runBeforeEach(to, from, signal2) {
406
+ if (signal2.aborted) throw new Error("Navigation aborted");
358
407
  for (const guard of this.beforeEachGuards) {
359
408
  if (signal2.aborted) throw new Error("Navigation aborted");
360
409
  const result = await this.runGuard(guard, to, from, signal2);
@@ -363,6 +412,7 @@ var GuardManager = class {
363
412
  return true;
364
413
  }
365
414
  async runBeforeResolve(to, from, signal2) {
415
+ if (signal2.aborted) throw new Error("Navigation aborted");
366
416
  for (const guard of this.beforeResolveGuards) {
367
417
  if (signal2.aborted) throw new Error("Navigation aborted");
368
418
  const result = await this.runGuard(guard, to, from, signal2);
@@ -459,13 +509,16 @@ var _ComponentLoader = class _ComponentLoader {
459
509
  constructor(cacheSize = 50, retryDelay = 1e3) {
460
510
  this.errorCache = /* @__PURE__ */ new Map();
461
511
  this.loadingPromises = /* @__PURE__ */ new Map();
512
+ // In-flight *module* resolutions for deferred plans, so concurrent first
513
+ // mounts of a lazy route do not each run the loader arrow.
514
+ this.moduleLoads = /* @__PURE__ */ new Map();
462
515
  // Stable per-route-definition id. Caching by the RESOLVED path (e.g.
463
516
  // /users/123) gave the cache one entry per visited URL, so parameterized
464
517
  // routes thrashed/evicted and the component was reloaded every navigation.
465
518
  // Keying by route-definition identity makes the cache effective again.
466
519
  this.routeKeys = /* @__PURE__ */ new WeakMap();
467
520
  this.keyCounter = 0;
468
- this.componentCache = new LRUCache(cacheSize);
521
+ this.planCache = new LRUCache(cacheSize);
469
522
  this.retryDelay = retryDelay;
470
523
  }
471
524
  keyFor(route2) {
@@ -476,91 +529,233 @@ var _ComponentLoader = class _ComponentLoader {
476
529
  }
477
530
  return key;
478
531
  }
479
- async loadComponent(route2, routePath) {
532
+ /**
533
+ * Resolve the route's **plan** — how to produce an instance — without ever
534
+ * instantiating a component to inspect it.
535
+ *
536
+ * Loading a route component and instantiating one are separate operations.
537
+ * The loader never invokes a user component factory merely to validate its
538
+ * return type; validation happens on the instance that will actually be
539
+ * mounted. (LOAD-001)
540
+ */
541
+ async loadPlan(route2, routePath) {
480
542
  if (!("component" in route2)) {
481
543
  throw new Error(`Route ${routePath} does not have a component`);
482
544
  }
483
545
  const comp = route2.component;
484
546
  const cacheKey = this.keyFor(route2);
485
- const cached = this.componentCache.get(cacheKey);
547
+ const cached = this.planCache.get(cacheKey);
486
548
  if (cached) return cached;
487
549
  const existingPromise = this.loadingPromises.get(cacheKey);
488
550
  if (existingPromise) return existingPromise;
489
- const errorInfo = this.errorCache.get(cacheKey);
490
- if (errorInfo && Date.now() - errorInfo.timestamp < this.retryDelay) {
491
- throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
492
- }
493
- const loadingPromise = this.doLoadComponent(comp, routePath);
551
+ this.assertNotHot(cacheKey);
552
+ const loadingPromise = this.doLoadPlan(comp, routePath);
494
553
  this.loadingPromises.set(cacheKey, loadingPromise);
495
554
  try {
496
- const component = await loadingPromise;
497
- this.componentCache.set(cacheKey, component);
555
+ const plan = await loadingPromise;
556
+ this.planCache.set(cacheKey, plan);
498
557
  this.errorCache.delete(cacheKey);
499
- return component;
558
+ return plan;
500
559
  } catch (error) {
501
- const currentError = this.errorCache.get(cacheKey) || { timestamp: 0, count: 0 };
502
- if (!this.errorCache.has(cacheKey) && this.errorCache.size >= _ComponentLoader.MAX_ERROR_ENTRIES) {
503
- const oldest = this.errorCache.keys().next().value;
504
- if (oldest !== void 0) this.errorCache.delete(oldest);
505
- }
506
- this.errorCache.set(cacheKey, {
507
- timestamp: Date.now(),
508
- count: currentError.count + 1
509
- });
560
+ this.noteError(cacheKey);
510
561
  throw error;
511
562
  } finally {
512
563
  this.loadingPromises.delete(cacheKey);
513
564
  }
514
565
  }
515
- async doLoadComponent(comp, routePath) {
516
- if (!this.isAsyncComponent(comp)) {
517
- const result = comp();
518
- if (!this.isElement(result)) {
519
- throw new Error(`Component for route "${routePath}" must return Element, got ${typeof result}`);
520
- }
521
- return comp;
566
+ /** Reject early while a recent load failure for this route is still hot. */
567
+ assertNotHot(cacheKey) {
568
+ const errorInfo = this.errorCache.get(cacheKey);
569
+ if (errorInfo && Date.now() - errorInfo.timestamp < this.retryDelay) {
570
+ throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
522
571
  }
572
+ }
573
+ /**
574
+ * Record a load failure. Keyed by route-definition id, so the map is bounded
575
+ * by the number of route definitions; still capped defensively, evicting the
576
+ * oldest.
577
+ */
578
+ noteError(cacheKey) {
579
+ const currentError = this.errorCache.get(cacheKey) || { timestamp: 0, count: 0 };
580
+ if (!this.errorCache.has(cacheKey) && this.errorCache.size >= _ComponentLoader.MAX_ERROR_ENTRIES) {
581
+ const oldest = this.errorCache.keys().next().value;
582
+ if (oldest !== void 0) this.errorCache.delete(oldest);
583
+ }
584
+ this.errorCache.set(cacheKey, { timestamp: Date.now(), count: currentError.count + 1 });
585
+ }
586
+ /**
587
+ * Classify without instantiating.
588
+ *
589
+ * The `lazy()` brand decides, and nothing else: a branded loader is
590
+ * `deferred`, everything else is a `factory`. No inspection of the function's
591
+ * type or source is involved, so an unbranded factory can never become
592
+ * preload-executable. (LOAD-004)
593
+ *
594
+ * An unbranded factory that happens to resolve to a module — an un-marked
595
+ * `async () => ({ default: Page })` — still works: that is unwrapped during
596
+ * real instantiation, where the produced value is used rather than discarded.
597
+ */
598
+ async doLoadPlan(comp, routePath) {
599
+ if (typeof comp !== "function") {
600
+ throw new Error(`Component for route "${routePath}" must be a function, got ${typeof comp}`);
601
+ }
602
+ if (!this.isDeferredModule(comp)) {
603
+ return { kind: "factory", component: comp };
604
+ }
605
+ return { kind: "deferred", load: comp };
606
+ }
607
+ /**
608
+ * Create **one** real route component instance, validating what will actually
609
+ * be mounted.
610
+ *
611
+ * `route` is used only to upgrade a `deferred` plan once a lazy module has
612
+ * resolved, so the import happens once and later mounts call the module's
613
+ * factory directly.
614
+ */
615
+ async instantiate(plan, route2, routePath) {
616
+ if (plan.kind === "factory") {
617
+ return this.instantiateFactory(plan.component, route2, routePath);
618
+ }
619
+ const factory = await this.resolveModuleFactory(plan, route2, routePath);
620
+ return this.instantiateFactory(factory, route2, routePath);
621
+ }
622
+ /**
623
+ * Resolve a deferred plan's module and cache the factory it exports —
624
+ * **without invoking that factory**.
625
+ *
626
+ * Shared by instantiation and preloading, so a preload racing a navigation to
627
+ * the same route imports the module once. A deferred plan is only ever a
628
+ * module loader (see `isDeferredModule`), so a resolution that is not
629
+ * module-shaped is a configuration error and is reported as one.
630
+ */
631
+ async resolveModuleFactory(plan, route2, routePath) {
632
+ const cacheKey = this.keyFor(route2);
633
+ const cached = this.planCache.get(cacheKey);
634
+ if (cached && cached.kind === "factory") return cached.component;
635
+ this.assertNotHot(cacheKey);
636
+ let pending = this.moduleLoads.get(cacheKey);
637
+ const owner = pending === void 0;
638
+ if (pending === void 0) {
639
+ pending = plan.load();
640
+ this.moduleLoads.set(cacheKey, pending);
641
+ }
642
+ let resolved;
523
643
  try {
524
- const result = await comp();
525
- const component = this.extractComponent(result, routePath);
526
- const testElement = component();
527
- if (!this.isElement(testElement)) {
528
- throw new Error(`Component for route "${routePath}" must return Element, got ${typeof testElement}`);
529
- }
530
- return component;
644
+ resolved = await pending;
531
645
  } catch (error) {
532
- const wrapped = new Error(
533
- `Failed to load component for route "${routePath}": ${error instanceof Error ? error.message : String(error)}`
534
- );
535
- wrapped.cause = error;
536
- throw wrapped;
646
+ this.noteError(cacheKey);
647
+ throw error;
648
+ } finally {
649
+ if (owner) this.moduleLoads.delete(cacheKey);
537
650
  }
651
+ const factory = this.extractFactory(resolved, routePath);
652
+ this.planCache.set(cacheKey, { kind: "factory", component: factory });
653
+ return factory;
538
654
  }
539
- isAsyncComponent(comp) {
540
- return comp[LAZY_MARKER] === true || comp.constructor.name === "AsyncFunction" || typeof comp === "function" && comp.toString().includes("import(");
655
+ /**
656
+ * Invoke a factory exactly once and validate the instance it produced.
657
+ *
658
+ * A factory may turn out to resolve to a *module* rather than an Element —
659
+ * an un-marked `async () => ({ default: Page })` is indistinguishable from a
660
+ * component factory until it runs. That is settled here, on the real
661
+ * invocation, and the plan is upgraded so later mounts skip the extra hop.
662
+ * `depth` bounds that unwrapping; a module whose default is itself a module
663
+ * is a configuration error, not something to chase.
664
+ */
665
+ async instantiateFactory(component, route2, routePath, depth = 0) {
666
+ const result = component();
667
+ const value = this.isThenable(result) ? await result : result;
668
+ if (this.isElement(value)) return value;
669
+ if (depth === 0 && this.isFactoryLike(value)) {
670
+ const factory = this.extractFactory(value, routePath);
671
+ this.planCache.set(this.keyFor(route2), { kind: "factory", component: factory });
672
+ return this.instantiateFactory(factory, route2, routePath, depth + 1);
673
+ }
674
+ throw new Error(`Component for route "${routePath}" must return Element, got ${typeof value}`);
541
675
  }
542
- isElement(value) {
543
- return value instanceof Element;
676
+ /**
677
+ * Something `extractFactory` can turn into a component factory: a bare
678
+ * callable, or a module namespace carrying a callable `default`. Both are
679
+ * long-supported shapes for an async loader's resolution.
680
+ */
681
+ isFactoryLike(value) {
682
+ if (typeof value === "function") return true;
683
+ return typeof value === "object" && value !== null && typeof value.default === "function";
544
684
  }
545
- extractComponent(result, routePath) {
546
- if ("default" in result && typeof result.default === "function") {
685
+ /**
686
+ * Pull a factory out of a resolved lazy module **without calling it**. Only
687
+ * what can be checked without running user code is checked here: that a
688
+ * default export exists and is callable.
689
+ */
690
+ extractFactory(result, routePath) {
691
+ if (typeof result === "function") return result;
692
+ if (typeof result === "object" && result !== null && typeof result.default === "function") {
547
693
  return result.default;
548
694
  }
549
- if (typeof result === "function") {
550
- return result;
551
- }
552
- if (this.isElement(result)) {
553
- return () => result;
554
- }
555
695
  throw new Error(`Invalid component module for route "${routePath}"`);
556
696
  }
697
+ /**
698
+ * Prepare explicitly deferred route code without creating a component
699
+ * instance or any DOM.
700
+ *
701
+ * Only routes explicitly branded as deferred — a loader wrapped in `lazy()` —
702
+ * are resolved during preload. Direct component factories, whether
703
+ * synchronous, `async`, or Promise-returning, are `factory` plans and
704
+ * therefore require no preload work at all.
705
+ *
706
+ * ```text
707
+ * factory plan → preload no-op
708
+ * deferred plan → resolve module → cache factory → no component invocation
709
+ * ```
710
+ *
711
+ * Deferred module resolution caches the component factory the module exports
712
+ * **without invoking it**. (LOAD-004)
713
+ */
714
+ async preloadPlan(route2, routePath) {
715
+ const plan = await this.loadPlan(route2, routePath);
716
+ if (plan.kind === "factory") return;
717
+ await this.resolveModuleFactory(plan, route2, routePath);
718
+ }
719
+ isThenable(value) {
720
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
721
+ }
722
+ /**
723
+ * Is this route component an **explicitly declared module loader**?
724
+ *
725
+ * Preload is the only place SibuJS executes a route function before the
726
+ * router needs an instance, so permission to do that must be explicit. The
727
+ * brand stamped by `lazy()` is the sole authority.
728
+ *
729
+ * Everything the loader once inferred is deliberately *not* a signal:
730
+ *
731
+ * - `constructor.name === "AsyncFunction"` — cannot distinguish
732
+ * `async () => import("./Page")` from `async () => div()`. Treating the
733
+ * latter as a module loader is what made preload invoke user
734
+ * components. (LOAD-003)
735
+ * - `toString().includes("import(")` — source text is a representation, not
736
+ * semantics. An ordinary component with `"import('./x')"` in a string, a
737
+ * template literal, or a comment was executed during preload; and the same
738
+ * text can vanish under a bundler that rewrites dynamic imports into its
739
+ * own chunk-loader calls. Both directions are wrong, and no better regex
740
+ * fixes that — the defect is source inspection itself. (LOAD-004)
741
+ *
742
+ * A brand is metadata: it survives TypeScript, every bundler, and
743
+ * minification, because it is part of runtime behaviour rather than syntax.
744
+ */
745
+ isDeferredModule(comp) {
746
+ return comp[LAZY_MARKER] === true;
747
+ }
748
+ isElement(value) {
749
+ return value instanceof Element;
750
+ }
557
751
  clearErrors() {
558
752
  this.errorCache.clear();
559
753
  }
560
754
  clearCache() {
561
- this.componentCache.clear();
755
+ this.planCache.clear();
562
756
  this.errorCache.clear();
563
757
  this.loadingPromises.clear();
758
+ this.moduleLoads.clear();
564
759
  }
565
760
  };
566
761
  _ComponentLoader.MAX_ERROR_ENTRIES = 256;
@@ -569,6 +764,17 @@ var _SibuRouter = class _SibuRouter {
569
764
  constructor(routes, options = {}) {
570
765
  // Event listeners cleanup
571
766
  this.cleanup = [];
767
+ // Set by destroy(). Lets a navigation still in flight report
768
+ // "router-destroyed" rather than the generic "superseded".
769
+ this.destroyed = false;
770
+ // Monotonic navigation generation. Bumped by every committed navigation that
771
+ // can acquire DOM ownership, and by destroy(). Async work that wants to
772
+ // commit into the DOM captures this first and re-checks before committing:
773
+ // unlike a URL comparison it distinguishes "/b" generation 10 from "/b"
774
+ // generation 12, so an A→B→A round trip can never restore commit permission
775
+ // to superseded work. The router's own initial location resolution is
776
+ // excluded — it establishes the bootstrap route rather than superseding it.
777
+ this.navEpoch = 0;
572
778
  this.options = {
573
779
  mode: "history",
574
780
  base: "",
@@ -606,7 +812,9 @@ var _SibuRouter = class _SibuRouter {
606
812
  }
607
813
  }
608
814
  queueMicrotask(() => {
609
- this.handleLocationChange(true);
815
+ if (!this.navigator.isNavigating) {
816
+ this.handleLocationChange(true, true);
817
+ }
610
818
  this.isReadySetter(true);
611
819
  });
612
820
  }
@@ -620,14 +828,15 @@ var _SibuRouter = class _SibuRouter {
620
828
  matched: []
621
829
  };
622
830
  }
623
- handleLocationChange(skipHistory = true) {
831
+ handleLocationChange(skipHistory = true, initialResolution = false) {
624
832
  const path = this.getCurrentPath();
625
- this.navigate(path, { replace: true, skipHistory }).catch((err) => {
833
+ this.navigate(path, { replace: true, skipHistory, initialResolution }).catch((err) => {
626
834
  console.error("[Router] Error during location change navigation:", err);
627
835
  });
628
836
  }
629
837
  getCurrentPath() {
630
838
  const { mode, base } = this.options;
839
+ if (typeof window === "undefined") return "/";
631
840
  if (mode === "hash") {
632
841
  return window.location.hash.slice(1) || "/";
633
842
  }
@@ -659,15 +868,15 @@ var _SibuRouter = class _SibuRouter {
659
868
  try {
660
869
  await this.navigator.navigate(async (signal2) => {
661
870
  const targetPath = this.resolvePath(to);
662
- if (!isSafeNavigationTarget(targetPath)) {
871
+ if (!isRouterNavigable(targetPath)) {
663
872
  const from2 = this.currentRouteGetter();
664
873
  const toContext2 = this.createRouteContext(targetPath);
665
- throw new NavigationFailureError("aborted", from2, toContext2);
874
+ throw new NavigationFailureError("aborted", from2, toContext2, void 0, "unsafe-target");
666
875
  }
667
876
  const from = this.currentRouteGetter();
668
877
  const toContext = this.createRouteContext(targetPath);
669
878
  if (this.isSameRoute(from, toContext)) {
670
- throw new NavigationFailureError("duplicated", from, toContext);
879
+ throw new NavigationFailureError("duplicated", from, toContext, void 0, "duplicate");
671
880
  }
672
881
  await this.performNavigation(toContext, from, options, signal2);
673
882
  });
@@ -675,30 +884,43 @@ var _SibuRouter = class _SibuRouter {
675
884
  } catch (error) {
676
885
  if (error instanceof NavigationFailureError) {
677
886
  const failure2 = error.toFailure();
678
- return { success: false, type: failure2.type, failure: failure2 };
887
+ return { success: false, type: failure2.type, reason: failure2.reason, failure: failure2 };
679
888
  }
889
+ const isAbort = error instanceof Error && error.message === "Navigation aborted";
890
+ const reason = isAbort ? this.destroyed ? "router-destroyed" : "superseded" : "error";
680
891
  const failure = {
681
892
  type: "aborted",
893
+ reason,
682
894
  from: this.currentRouteGetter(),
683
895
  to: this.createRouteContext(this.resolvePath(to)),
684
896
  error: error instanceof Error ? error : new Error(String(error))
685
897
  };
686
- return { success: false, type: failure.type, failure };
898
+ return { success: false, type: failure.type, reason, failure };
687
899
  }
688
900
  }
689
- async performNavigation(to, from, options, signal2, depth = 0) {
901
+ async performNavigation(to, from, options, signal2, depth = 0, redirectChain = []) {
902
+ const chain = depth === 0 ? [to.path] : redirectChain;
690
903
  if (depth >= _SibuRouter.MAX_REDIRECT_DEPTH) {
691
- throw new NavigationFailureError("aborted", from, to);
904
+ if (isDev()) {
905
+ devWarn(
906
+ `Router: redirect loop detected \u2014 navigation aborted after ${_SibuRouter.MAX_REDIRECT_DEPTH} hops.
907
+ ${chain.map((p) => `"${p}"`).join(" -> ")}`
908
+ );
909
+ }
910
+ throw new NavigationFailureError("aborted", from, to, void 0, "redirect-loop");
692
911
  }
693
912
  const beforeEachResult = await this.guards.runBeforeEach(to, from, signal2);
694
913
  if (beforeEachResult !== true) {
695
914
  if (typeof beforeEachResult === "string") {
696
- if (!isSafeNavigationTarget(beforeEachResult)) {
697
- throw new NavigationFailureError("aborted", from, to);
915
+ if (!isRouterNavigable(beforeEachResult)) {
916
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
698
917
  }
699
- return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1);
918
+ return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1, [
919
+ ...chain,
920
+ beforeEachResult
921
+ ]);
700
922
  }
701
- throw new NavigationFailureError("aborted", from, to);
923
+ throw new NavigationFailureError("aborted", from, to, void 0, "guard");
702
924
  }
703
925
  const match = this.matcher.match(to.path);
704
926
  if (match) {
@@ -709,47 +931,60 @@ var _SibuRouter = class _SibuRouter {
709
931
  for (const guard of guards) {
710
932
  if (signal2.aborted) throw new Error("Navigation aborted");
711
933
  const result = await guard(to, from);
934
+ if (signal2.aborted) {
935
+ throw new NavigationFailureError("aborted", from, to, void 0, abortReasonOf(signal2));
936
+ }
712
937
  if (result !== true) {
713
938
  if (typeof result === "string") {
714
- if (!isSafeNavigationTarget(result)) {
715
- throw new NavigationFailureError("aborted", from, to);
939
+ if (!isRouterNavigable(result)) {
940
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
716
941
  }
717
- return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1);
942
+ return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1, [
943
+ ...chain,
944
+ result
945
+ ]);
718
946
  }
719
- throw new NavigationFailureError("aborted", from, to);
947
+ throw new NavigationFailureError("aborted", from, to, void 0, "guard");
720
948
  }
721
949
  }
722
950
  }
723
951
  }
724
952
  if ("redirect" in route2) {
725
953
  const redirectPath = typeof route2.redirect === "function" ? route2.redirect(to) : route2.redirect;
726
- if (typeof redirectPath === "string" && /^(https?:)?\/\//i.test(redirectPath)) {
727
- if (typeof console !== "undefined") {
954
+ if (typeof redirectPath === "string" && !isRouterNavigable(redirectPath)) {
955
+ if (classifyNavigationTarget(redirectPath) === "external" && typeof console !== "undefined") {
728
956
  console.error(
729
957
  `[SibuJS Router] Refusing absolute/protocol-relative redirect "${redirectPath}" \u2014 open-redirect risk.`
730
958
  );
731
959
  }
732
- throw new NavigationFailureError("aborted", from, to);
960
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
733
961
  }
734
- if (typeof redirectPath === "string" && !isSafeNavigationTarget(redirectPath)) {
735
- throw new NavigationFailureError("aborted", from, to);
736
- }
737
- return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1);
962
+ return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1, [
963
+ ...chain,
964
+ redirectPath
965
+ ]);
738
966
  }
739
967
  }
740
968
  const beforeResolveResult = await this.guards.runBeforeResolve(to, from, signal2);
741
969
  if (beforeResolveResult !== true) {
742
970
  if (typeof beforeResolveResult === "string") {
743
- if (!isSafeNavigationTarget(beforeResolveResult)) {
744
- throw new NavigationFailureError("aborted", from, to);
971
+ if (!isRouterNavigable(beforeResolveResult)) {
972
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
745
973
  }
746
- return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1);
974
+ return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1, [
975
+ ...chain,
976
+ beforeResolveResult
977
+ ]);
747
978
  }
748
- throw new NavigationFailureError("aborted", from, to);
979
+ throw new NavigationFailureError("aborted", from, to, void 0, "guard");
980
+ }
981
+ if (signal2.aborted) {
982
+ throw new NavigationFailureError("aborted", from, to, void 0, abortReasonOf(signal2));
749
983
  }
750
984
  if (!options.skipHistory) {
751
985
  this.updateHistory(to, options);
752
986
  }
987
+ if (!options.initialResolution) this.navEpoch++;
753
988
  this.currentRouteSetter(to);
754
989
  this.guards.runAfterEach(to, from);
755
990
  this.handleScrollBehavior(to, from);
@@ -777,29 +1012,76 @@ var _SibuRouter = class _SibuRouter {
777
1012
  return path;
778
1013
  }
779
1014
  isSameRoute(from, to) {
1015
+ if (from.matched.length === 0 && to.matched.length > 0) return false;
780
1016
  return from.path === to.path && JSON.stringify(from.params) === JSON.stringify(to.params) && JSON.stringify(from.query) === JSON.stringify(to.query) && from.hash === to.hash;
781
1017
  }
782
1018
  updateHistory(to, options) {
783
1019
  const fullPath = this.options.base + to.path + (Object.keys(to.query).length ? `?${new URLSearchParams(to.query).toString()}` : "") + (to.hash ? `#${to.hash}` : "");
1020
+ const h = globalThis.history;
1021
+ if (!h) return;
784
1022
  if (options.replace) {
785
- history.replaceState(options.state || {}, "", fullPath);
1023
+ h.replaceState(options.state || {}, "", fullPath);
786
1024
  } else {
787
- history.pushState(options.state || {}, "", fullPath);
1025
+ h.pushState(options.state || {}, "", fullPath);
788
1026
  }
789
1027
  }
1028
+ /**
1029
+ * Apply the configured `scrollBehavior`, if the runtime can actually scroll.
1030
+ *
1031
+ * This runs *after* the route has been committed, so an exception here does
1032
+ * not merely skip scrolling — it propagates out of `navigateInternal` and the
1033
+ * navigation is reported as `success: false` while the route state says it
1034
+ * succeeded. In a DOM-less runtime the bare `requestAnimationFrame` did
1035
+ * exactly that: a legal `scrollBehavior` option made every navigation report
1036
+ * failure under SSR and in memory-router-style tests. (MEM-001)
1037
+ *
1038
+ * Policy A, consistent with the history write in `updateHistory()` (NODE-001):
1039
+ * the route still commits, and only the browser-only side effect is skipped.
1040
+ *
1041
+ * Both primitives are probed, not just `window` — `requestAnimationFrame` and
1042
+ * `scrollTo` can be missing independently (jsdom without a rAF polyfill, a
1043
+ * partial DOM shim). `requestAnimationFrame` is invoked as a method on the
1044
+ * global object rather than through a detached reference, since browsers
1045
+ * reject a bare call with "Illegal invocation".
1046
+ */
790
1047
  handleScrollBehavior(to, from) {
791
- if (this.options.scrollBehavior) {
792
- const scrollTo = this.options.scrollBehavior(to, from, null);
793
- if (scrollTo) {
794
- requestAnimationFrame(() => {
795
- window.scrollTo(scrollTo.x, scrollTo.y);
796
- });
797
- }
1048
+ const scrollBehavior = this.options.scrollBehavior;
1049
+ if (!scrollBehavior) return;
1050
+ const g = globalThis;
1051
+ if (typeof g.requestAnimationFrame !== "function") return;
1052
+ if (typeof window === "undefined" || typeof window.scrollTo !== "function") return;
1053
+ let position;
1054
+ try {
1055
+ position = scrollBehavior(to, from, null);
1056
+ } catch (error) {
1057
+ if (typeof console !== "undefined") console.error("[router] scrollBehavior failed:", error);
1058
+ return;
798
1059
  }
1060
+ if (!position) return;
1061
+ const { x, y } = position;
1062
+ g.requestAnimationFrame(() => {
1063
+ try {
1064
+ window.scrollTo(x, y);
1065
+ } catch (error) {
1066
+ if (typeof console !== "undefined") console.error("[router] scroll failed:", error);
1067
+ }
1068
+ });
799
1069
  }
800
1070
  // Component loading
801
- async loadComponent(route2, routePath) {
802
- return this.loader.loadComponent(route2, routePath);
1071
+ /**
1072
+ * Resolve how this route's component will be produced, without instantiating
1073
+ * anything. Pair with {@link instantiateComponent} at the commit boundary.
1074
+ */
1075
+ async loadPlan(route2, routePath) {
1076
+ return this.loader.loadPlan(route2, routePath);
1077
+ }
1078
+ /** Create exactly one real instance from an already-resolved plan. */
1079
+ async instantiateComponent(plan, route2, routePath) {
1080
+ return this.loader.instantiate(plan, route2, routePath);
1081
+ }
1082
+ /** Resolve a route's module/factory without creating any component instance. */
1083
+ async preloadPlan(route2, routePath) {
1084
+ return this.loader.preloadPlan(route2, routePath);
803
1085
  }
804
1086
  // Guards API
805
1087
  beforeEach(guard) {
@@ -834,12 +1116,18 @@ var _SibuRouter = class _SibuRouter {
834
1116
  get isReady() {
835
1117
  return this.isReadyGetter();
836
1118
  }
1119
+ /** @internal Navigation generation — see `navEpoch`. Not a public API. */
1120
+ get navigationEpoch() {
1121
+ return this.navEpoch;
1122
+ }
837
1123
  get isNavigating() {
838
1124
  return this.navigator.isNavigating;
839
1125
  }
840
1126
  // Cleanup
841
1127
  destroy() {
842
- this.navigator.abort();
1128
+ this.destroyed = true;
1129
+ this.navEpoch++;
1130
+ this.navigator.abort(ABORT_ROUTER_DESTROYED);
843
1131
  for (const fn of this.cleanup) fn();
844
1132
  this.cleanup = [];
845
1133
  this.guards.clear();
@@ -882,10 +1170,11 @@ var _SibuRouter = class _SibuRouter {
882
1170
  _SibuRouter.MAX_REDIRECT_DEPTH = 10;
883
1171
  var SibuRouter = _SibuRouter;
884
1172
  var NavigationFailureError = class extends Error {
885
- constructor(type, from, to, error) {
886
- super(`Navigation ${type}: from ${from.path} to ${to.path}`);
1173
+ constructor(type, from, to, error, reason) {
1174
+ super(`Navigation ${type}${reason ? ` (${reason})` : ""}: from ${from.path} to ${to.path}`);
887
1175
  this.name = "NavigationFailureError";
888
1176
  this.type = type;
1177
+ this.reason = reason;
889
1178
  this.from = from;
890
1179
  this.to = to;
891
1180
  if (error) {
@@ -895,12 +1184,16 @@ var NavigationFailureError = class extends Error {
895
1184
  toFailure() {
896
1185
  return {
897
1186
  type: this.type,
1187
+ reason: this.reason,
898
1188
  from: this.from,
899
1189
  to: this.to,
900
1190
  error: this.cause instanceof Error ? this.cause : void 0
901
1191
  };
902
1192
  }
903
1193
  };
1194
+ function abortReasonOf(signal2) {
1195
+ return signal2.reason === ABORT_ROUTER_DESTROYED ? "router-destroyed" : "superseded";
1196
+ }
904
1197
  var ROUTER_KEY = /* @__PURE__ */ Symbol.for("sibujs.router.v1");
905
1198
  var _routerRef = globalThis[ROUTER_KEY] ?? (globalThis[ROUTER_KEY] = { current: null });
906
1199
  function normalizeRoutes(routes) {
@@ -1004,6 +1297,13 @@ function Route() {
1004
1297
  let errorNode = null;
1005
1298
  let navSeq = 0;
1006
1299
  let currentTopRoute = null;
1300
+ let routeTorn = false;
1301
+ const commitTarget = (seq) => routeTorn || seq !== navSeq ? null : anchor.parentNode;
1302
+ const release = (node) => {
1303
+ if (!node) return;
1304
+ dispose(node);
1305
+ node.parentNode?.removeChild(node);
1306
+ };
1007
1307
  const cleanupNodes = () => {
1008
1308
  [currentNode, loadingNode, errorNode].forEach((node) => {
1009
1309
  if (!node) return;
@@ -1081,7 +1381,7 @@ function Route() {
1081
1381
  anchor.parentNode.insertBefore(errorNode, anchor.nextSibling);
1082
1382
  };
1083
1383
  const update = async () => {
1084
- if (!_routerRef.current) return;
1384
+ if (routeTorn || !_routerRef.current) return;
1085
1385
  const seq = ++navSeq;
1086
1386
  const route2 = _routerRef.current.currentRoute;
1087
1387
  try {
@@ -1110,17 +1410,22 @@ function Route() {
1110
1410
  if (isAsync) {
1111
1411
  showLoading();
1112
1412
  }
1113
- const component = await _routerRef.current.loadComponent(routeDef, route2.path);
1114
- if (seq !== navSeq) return;
1115
- const node = component();
1116
- if (node && anchor.parentNode) {
1413
+ const plan = await _routerRef.current.loadPlan(routeDef, route2.path);
1414
+ if (!commitTarget(seq)) return;
1415
+ const node = await _routerRef.current.instantiateComponent(plan, routeDef, route2.path);
1416
+ const parent = commitTarget(seq);
1417
+ if (!parent) {
1418
+ release(node);
1419
+ return;
1420
+ }
1421
+ if (node) {
1117
1422
  currentTopRoute = routeDef;
1118
1423
  cleanupNodes();
1119
- anchor.parentNode.insertBefore(node, anchor.nextSibling);
1424
+ parent.insertBefore(node, anchor.nextSibling);
1120
1425
  currentNode = node;
1121
1426
  }
1122
1427
  } catch (error) {
1123
- if (seq !== navSeq) return;
1428
+ if (routeTorn || seq !== navSeq) return;
1124
1429
  hideLoading();
1125
1430
  console.error("[Route] Component error:", error);
1126
1431
  showError(error instanceof Error ? error : new Error(String(error)), routeDef);
@@ -1128,6 +1433,7 @@ function Route() {
1128
1433
  }
1129
1434
  } catch (error) {
1130
1435
  if (seq !== navSeq) return;
1436
+ if (routeTorn) return;
1131
1437
  console.error("[Route] Update failed:", error);
1132
1438
  showError(error instanceof Error ? error : new Error(String(error)));
1133
1439
  }
@@ -1144,12 +1450,13 @@ function Route() {
1144
1450
  if (!routeInitialized && anchor.parentNode) wrappedUpdate();
1145
1451
  });
1146
1452
  }
1147
- let routeTorn = false;
1148
1453
  const routeCleanup = () => {
1149
1454
  if (routeTorn) return;
1150
1455
  routeTorn = true;
1456
+ navSeq++;
1151
1457
  routeTeardown();
1152
1458
  cleanupNodes();
1459
+ currentTopRoute = null;
1153
1460
  };
1154
1461
  registerDisposer(anchor, routeCleanup);
1155
1462
  routeCleanups.push(routeCleanup);
@@ -1167,16 +1474,14 @@ function KeepAliveRoute(options) {
1167
1474
  let currentNode = null;
1168
1475
  let currentKey = "";
1169
1476
  let currentCached = false;
1170
- let isUpdating = false;
1171
- let pendingUpdate = false;
1477
+ let updateSeq = 0;
1478
+ let kaTorn = false;
1172
1479
  const update = async () => {
1173
- if (!_routerRef.current) return;
1174
- if (isUpdating) {
1175
- pendingUpdate = true;
1176
- return;
1177
- }
1178
- const route2 = _routerRef.current.currentRoute;
1179
- const match = _routerRef.current["matcher"].match(route2.path);
1480
+ if (kaTorn || !_routerRef.current) return;
1481
+ const seq = ++updateSeq;
1482
+ const router2 = _routerRef.current;
1483
+ const route2 = router2.currentRoute;
1484
+ const match = router2["matcher"].match(route2.path);
1180
1485
  if (!match) return;
1181
1486
  const { route: routeDef } = match;
1182
1487
  if ("redirect" in routeDef) {
@@ -1193,63 +1498,62 @@ function KeepAliveRoute(options) {
1193
1498
  const cacheKey = `${route2.path}${queryStr}${route2.hash ? `#${route2.hash}` : ""}`;
1194
1499
  const shouldCache = !includeNames || routeDef.name != null && includeNames.includes(routeDef.name);
1195
1500
  if (cacheKey === currentKey && currentNode) return;
1196
- isUpdating = true;
1501
+ if (!anchor.parentNode) return;
1502
+ let node = null;
1503
+ let fromCache = false;
1504
+ if (shouldCache && cache.has(cacheKey)) {
1505
+ node = cache.get(cacheKey);
1506
+ fromCache = true;
1507
+ } else {
1508
+ try {
1509
+ const plan = await router2.loadPlan(routeDef, route2.path);
1510
+ if (kaTorn || seq !== updateSeq) return;
1511
+ node = await router2.instantiateComponent(plan, routeDef, route2.path);
1512
+ } catch (error) {
1513
+ if (kaTorn || seq !== updateSeq) return;
1514
+ console.error("[KeepAliveRoute] Component error:", error);
1515
+ return;
1516
+ }
1517
+ if (!node) return;
1518
+ if (kaTorn || seq !== updateSeq) {
1519
+ dispose(node);
1520
+ return;
1521
+ }
1522
+ }
1197
1523
  const parent = anchor.parentNode;
1198
1524
  if (!parent) {
1199
- isUpdating = false;
1525
+ if (!fromCache) dispose(node);
1200
1526
  return;
1201
1527
  }
1202
- try {
1203
- if (currentNode?.parentNode) {
1204
- parent.removeChild(currentNode);
1205
- if (!currentCached) {
1206
- dispose(currentNode);
1207
- }
1208
- }
1209
- if (shouldCache && cache.has(cacheKey)) {
1210
- currentNode = cache.get(cacheKey);
1211
- currentCached = true;
1212
- const idx = lruOrder.indexOf(cacheKey);
1213
- if (idx !== -1) {
1214
- lruOrder.splice(idx, 1);
1215
- }
1216
- lruOrder.push(cacheKey);
1217
- } else {
1218
- const component = await _routerRef.current.loadComponent(routeDef, route2.path);
1219
- const node = component();
1220
- if (!node || route2.path !== _routerRef.current.currentRoute.path) {
1221
- isUpdating = false;
1222
- return;
1528
+ if (currentNode && currentNode !== node) {
1529
+ if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
1530
+ if (!currentCached) dispose(currentNode);
1531
+ }
1532
+ if (fromCache) {
1533
+ const idx = lruOrder.indexOf(cacheKey);
1534
+ if (idx !== -1) lruOrder.splice(idx, 1);
1535
+ lruOrder.push(cacheKey);
1536
+ } else if (shouldCache) {
1537
+ cache.set(cacheKey, node);
1538
+ lruOrder.push(cacheKey);
1539
+ while (lruOrder.length > maxCache) {
1540
+ const evictKey = lruOrder.shift();
1541
+ if (evictKey === cacheKey) {
1542
+ lruOrder.push(evictKey);
1543
+ break;
1223
1544
  }
1224
- currentNode = node;
1225
- currentCached = shouldCache;
1226
- if (shouldCache) {
1227
- cache.set(cacheKey, node);
1228
- lruOrder.push(cacheKey);
1229
- while (lruOrder.length > maxCache) {
1230
- const evictKey = lruOrder.shift();
1231
- const evictNode = cache.get(evictKey);
1232
- if (evictNode) {
1233
- dispose(evictNode);
1234
- if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
1235
- cache.delete(evictKey);
1236
- }
1237
- }
1545
+ const evictNode = cache.get(evictKey);
1546
+ if (evictNode) {
1547
+ dispose(evictNode);
1548
+ if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
1549
+ cache.delete(evictKey);
1238
1550
  }
1239
1551
  }
1240
- currentKey = cacheKey;
1241
- if (currentNode) {
1242
- parent.insertBefore(currentNode, anchor.nextSibling);
1243
- }
1244
- } catch (error) {
1245
- console.error("[KeepAliveRoute] Component error:", error);
1246
- } finally {
1247
- isUpdating = false;
1248
- if (pendingUpdate) {
1249
- pendingUpdate = false;
1250
- update();
1251
- }
1252
1552
  }
1553
+ currentNode = node;
1554
+ currentCached = fromCache || shouldCache;
1555
+ currentKey = cacheKey;
1556
+ parent.insertBefore(node, anchor.nextSibling);
1253
1557
  };
1254
1558
  let initialized = false;
1255
1559
  const wrappedUpdate = async () => {
@@ -1262,10 +1566,10 @@ function KeepAliveRoute(options) {
1262
1566
  if (!initialized && anchor.parentNode) wrappedUpdate();
1263
1567
  });
1264
1568
  }
1265
- let kaTorn = false;
1266
1569
  const kaCleanup = () => {
1267
1570
  if (kaTorn) return;
1268
1571
  kaTorn = true;
1572
+ updateSeq++;
1269
1573
  kaTeardown();
1270
1574
  for (const node of cache.values()) {
1271
1575
  dispose(node);
@@ -1274,7 +1578,10 @@ function KeepAliveRoute(options) {
1274
1578
  cache.clear();
1275
1579
  lruOrder.length = 0;
1276
1580
  if (currentNode?.parentNode) currentNode.parentNode.removeChild(currentNode);
1581
+ if (currentNode && !currentCached) dispose(currentNode);
1277
1582
  currentNode = null;
1583
+ currentKey = "";
1584
+ currentCached = false;
1278
1585
  };
1279
1586
  registerDisposer(anchor, kaCleanup);
1280
1587
  routeCleanups.push(kaCleanup);
@@ -1287,8 +1594,12 @@ function RouterLink(props, children) {
1287
1594
  const baseClass = typeof classAttr === "string" ? classAttr : "";
1288
1595
  const routeGetter = _routerRef.current.routeGetter;
1289
1596
  const rawHref = _routerRef.current["resolvePath"](to);
1290
- const href = isSafeNavigationTarget(rawHref) ? rawHref : "#";
1291
- const hrefPath = href.split("?")[0].split("#")[0];
1597
+ const kind = classifyNavigationTarget(rawHref);
1598
+ const href = kind === "unsafe" ? "#" : rawHref;
1599
+ const [hrefBeforeHash, hrefHash = ""] = href.split("#");
1600
+ const [hrefPathRaw, hrefQueryRaw = ""] = hrefBeforeHash.split("?");
1601
+ const targetPath = normalizePathname(hrefPathRaw);
1602
+ const targetQuery = normalizeQuery(hrefQueryRaw);
1292
1603
  const link = document.createElement("a");
1293
1604
  link.href = href;
1294
1605
  if (target) {
@@ -1304,8 +1615,9 @@ function RouterLink(props, children) {
1304
1615
  const options = _routerRef.current["options"];
1305
1616
  const effectCleanup = effect(() => {
1306
1617
  const route2 = routeGetter();
1307
- const isActive = route2.path.startsWith(hrefPath);
1308
- const isExactActive = route2.path === hrefPath;
1618
+ const currentPath = normalizePathname(route2.path);
1619
+ const isActive = kind === "internal" && isPathnameAncestor(targetPath, currentPath);
1620
+ const isExactActive = kind === "internal" && currentPath === targetPath && normalizeQuery(route2.query) === targetQuery && route2.hash === hrefHash;
1309
1621
  const classes = [];
1310
1622
  if (isActive) {
1311
1623
  if (activeClass) classes.push(activeClass);
@@ -1327,7 +1639,7 @@ function RouterLink(props, children) {
1327
1639
  const safe = sanitizeUrl(str);
1328
1640
  if (safe) link.setAttribute(key, safe);
1329
1641
  } else if (lkey === "style") {
1330
- link.setAttribute(key, sanitizeCSSValue(str));
1642
+ link.setAttribute(key, sanitizeStyleAttribute(str));
1331
1643
  } else {
1332
1644
  link.setAttribute(key, str);
1333
1645
  }
@@ -1347,10 +1659,13 @@ function RouterLink(props, children) {
1347
1659
  });
1348
1660
  }
1349
1661
  const onLinkClick = (e) => {
1350
- if (target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
1662
+ if (e.defaultPrevented || target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
1351
1663
  return;
1352
1664
  }
1665
+ if (link.hasAttribute("download")) return;
1666
+ if (kind === "external") return;
1353
1667
  e.preventDefault();
1668
+ if (kind === "unsafe") return;
1354
1669
  _routerRef.current?.navigate(to, { replace: replace2 }).catch((err) => {
1355
1670
  if (typeof console !== "undefined") console.error("[router] link navigate failed:", err);
1356
1671
  });
@@ -1366,68 +1681,80 @@ function Suspense(props) {
1366
1681
  let currentNode = null;
1367
1682
  let fallbackNode = null;
1368
1683
  let isLoading = false;
1684
+ let generation = 0;
1685
+ let tornDown = false;
1686
+ const release = (node) => {
1687
+ if (!node) return;
1688
+ dispose(node);
1689
+ node.parentNode?.removeChild(node);
1690
+ };
1691
+ const commitTarget = (myGeneration) => tornDown || myGeneration !== generation ? null : anchor.parentNode;
1369
1692
  const cleanupNodes = () => {
1370
- [currentNode, fallbackNode].forEach((node) => {
1371
- if (node?.parentNode) {
1372
- node.parentNode.removeChild(node);
1373
- }
1374
- });
1693
+ release(currentNode);
1694
+ release(fallbackNode);
1375
1695
  currentNode = null;
1376
1696
  fallbackNode = null;
1377
1697
  };
1378
- const showFallback = () => {
1379
- if (fallbackNode || !props.fallback || !anchor.parentNode) return;
1698
+ const showFallback = (myGeneration) => {
1699
+ if (fallbackNode || !props.fallback || tornDown) return;
1700
+ if (!anchor.parentNode) return;
1701
+ let fallback;
1380
1702
  try {
1381
- const fallback = typeof props.fallback === "function" ? props.fallback() : props.fallback;
1382
- if (fallback instanceof HTMLElement) {
1383
- fallbackNode = fallback;
1384
- anchor.parentNode.insertBefore(fallbackNode, anchor.nextSibling);
1385
- }
1703
+ fallback = typeof props.fallback === "function" ? props.fallback() : props.fallback;
1386
1704
  } catch (error) {
1387
1705
  console.error("[Suspense] Fallback error:", error);
1706
+ return;
1388
1707
  }
1389
- };
1390
- const hideFallback = () => {
1391
- if (fallbackNode?.parentNode) {
1392
- fallbackNode.parentNode.removeChild(fallbackNode);
1393
- fallbackNode = null;
1708
+ if (!(fallback instanceof HTMLElement)) return;
1709
+ const parent = commitTarget(myGeneration);
1710
+ if (!parent) {
1711
+ release(fallback);
1712
+ return;
1394
1713
  }
1714
+ fallbackNode = fallback;
1715
+ parent.insertBefore(fallbackNode, anchor.nextSibling);
1395
1716
  };
1396
1717
  const render = async () => {
1397
- if (isLoading) return;
1718
+ if (tornDown || isLoading) return;
1398
1719
  isLoading = true;
1720
+ const myGeneration = ++generation;
1399
1721
  try {
1400
1722
  const result = props.nodes();
1723
+ let element;
1401
1724
  if (result instanceof Promise) {
1402
- showFallback();
1403
- const element = await result;
1404
- if (anchor.parentNode) {
1405
- cleanupNodes();
1406
- anchor.parentNode.insertBefore(element, anchor.nextSibling);
1407
- currentNode = element;
1408
- }
1725
+ showFallback(myGeneration);
1726
+ element = await result;
1409
1727
  } else {
1410
- if (anchor.parentNode) {
1411
- cleanupNodes();
1412
- anchor.parentNode.insertBefore(result, anchor.nextSibling);
1413
- currentNode = result;
1414
- }
1728
+ element = result;
1415
1729
  }
1416
- } catch (error) {
1417
- hideFallback();
1418
- console.error("[Suspense] Nodes error:", error);
1419
- if (anchor.parentNode) {
1420
- const errorElement = document.createElement("div");
1421
- errorElement.className = "suspense-error";
1422
- errorElement.textContent = error instanceof Error ? error.message : "Failed to load";
1730
+ const parent = commitTarget(myGeneration);
1731
+ if (!parent) {
1732
+ release(element);
1423
1733
  cleanupNodes();
1424
- anchor.parentNode.insertBefore(errorElement, anchor.nextSibling);
1425
- currentNode = errorElement;
1734
+ return;
1426
1735
  }
1736
+ cleanupNodes();
1737
+ parent.insertBefore(element, anchor.nextSibling);
1738
+ currentNode = element;
1739
+ } catch (error) {
1740
+ console.error("[Suspense] Nodes error:", error);
1741
+ const parent = commitTarget(myGeneration);
1742
+ cleanupNodes();
1743
+ if (!parent) return;
1744
+ const errorElement = document.createElement("div");
1745
+ errorElement.className = "suspense-error";
1746
+ errorElement.textContent = error instanceof Error ? error.message : "Failed to load";
1747
+ parent.insertBefore(errorElement, anchor.nextSibling);
1748
+ currentNode = errorElement;
1427
1749
  } finally {
1428
1750
  isLoading = false;
1429
1751
  }
1430
1752
  };
1753
+ registerDisposer(anchor, () => {
1754
+ tornDown = true;
1755
+ generation++;
1756
+ cleanupNodes();
1757
+ });
1431
1758
  queueMicrotask(render);
1432
1759
  return anchor;
1433
1760
  }
@@ -1442,12 +1769,15 @@ async function preloadRoute(to) {
1442
1769
  const match = _routerRef.current["matcher"].match(path.split("?")[0].split("#")[0]);
1443
1770
  if (match && "component" in match.route) {
1444
1771
  try {
1445
- await _routerRef.current.loadComponent(match.route, path);
1772
+ await _routerRef.current.preloadPlan(match.route, path);
1446
1773
  } catch (error) {
1447
1774
  console.warn("[Router] Preload failed:", error);
1448
1775
  }
1449
1776
  }
1450
1777
  }
1778
+ function __getNavigationEpoch() {
1779
+ return _routerRef.current?.navigationEpoch ?? -1;
1780
+ }
1451
1781
  function hasRoute(name) {
1452
1782
  if (!_routerRef.current) return false;
1453
1783
  return _routerRef.current["matcher"].findByName(name) !== null;
@@ -1489,6 +1819,7 @@ function Outlet() {
1489
1819
  let currentNode = null;
1490
1820
  let currentChild = null;
1491
1821
  let navSeq = 0;
1822
+ let outletTorn = false;
1492
1823
  const clearCurrent = () => {
1493
1824
  if (currentNode) {
1494
1825
  dispose(currentNode);
@@ -1497,8 +1828,14 @@ function Outlet() {
1497
1828
  }
1498
1829
  currentChild = null;
1499
1830
  };
1831
+ const commitTarget = (seq) => outletTorn || seq !== navSeq ? null : anchor.parentNode;
1832
+ const release = (node) => {
1833
+ if (!node) return;
1834
+ dispose(node);
1835
+ node.parentNode?.removeChild(node);
1836
+ };
1500
1837
  const update = async () => {
1501
- if (!_routerRef.current) return;
1838
+ if (outletTorn || !_routerRef.current) return;
1502
1839
  const seq = ++navSeq;
1503
1840
  const route2 = _routerRef.current.currentRoute;
1504
1841
  if (route2.matched.length < 2) {
@@ -1513,17 +1850,21 @@ function Outlet() {
1513
1850
  if (childRoute === currentChild && currentNode) return;
1514
1851
  try {
1515
1852
  const cacheKey = `${route2.path}\0${childRoute.path}`;
1516
- const component = await _routerRef.current.loadComponent(childRoute, cacheKey);
1517
- if (seq !== navSeq) return;
1518
- const node = component();
1519
- if (node && anchor.parentNode) {
1520
- clearCurrent();
1521
- anchor.parentNode.insertBefore(node, anchor.nextSibling);
1522
- currentNode = node;
1523
- currentChild = childRoute;
1853
+ const plan = await _routerRef.current.loadPlan(childRoute, cacheKey);
1854
+ if (!commitTarget(seq)) return;
1855
+ const node = await _routerRef.current.instantiateComponent(plan, childRoute, cacheKey);
1856
+ const parent = commitTarget(seq);
1857
+ if (!parent) {
1858
+ release(node);
1859
+ return;
1524
1860
  }
1861
+ if (!node) return;
1862
+ clearCurrent();
1863
+ parent.insertBefore(node, anchor.nextSibling);
1864
+ currentNode = node;
1865
+ currentChild = childRoute;
1525
1866
  } catch (error) {
1526
- if (seq !== navSeq) return;
1867
+ if (outletTorn || seq !== navSeq) return;
1527
1868
  console.error("[Outlet] Failed to render child route:", error);
1528
1869
  }
1529
1870
  };
@@ -1533,16 +1874,17 @@ function Outlet() {
1533
1874
  if (anchor.parentNode) update();
1534
1875
  });
1535
1876
  }
1536
- let outletTorn = false;
1537
1877
  const outletCleanup = () => {
1538
1878
  if (outletTorn) return;
1539
1879
  outletTorn = true;
1880
+ navSeq++;
1540
1881
  outletTeardown();
1541
1882
  if (currentNode) {
1542
1883
  dispose(currentNode);
1543
1884
  if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
1544
1885
  currentNode = null;
1545
1886
  }
1887
+ currentChild = null;
1546
1888
  };
1547
1889
  registerDisposer(anchor, outletCleanup);
1548
1890
  routeCleanups.push(outletCleanup);
@@ -1803,16 +2145,10 @@ function renderRouteToString(url, routes, _options) {
1803
2145
  function renderRouteToDocument(url, routes, options) {
1804
2146
  const { html, state } = renderRouteToString(url, routes, options);
1805
2147
  const opts = options || {};
1806
- const metaTags = (opts.meta || []).filter((attrs) => !isDangerousMetaRefresh(attrs)).map((attrs) => {
1807
- const pairs = buildSafeAttrString(attrs);
1808
- return pairs ? `<meta ${pairs} />` : "";
1809
- }).filter(Boolean).join("\n ");
1810
- const linkTags = (opts.links || []).map((attrs) => {
1811
- const pairs = buildSafeAttrString(attrs);
1812
- return pairs ? `<link ${pairs} />` : "";
1813
- }).filter(Boolean).join("\n ");
2148
+ const metaTags = (opts.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
2149
+ const linkTags = (opts.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
1814
2150
  const scriptTags = (opts.scripts || []).map((src) => {
1815
- const safe = sanitizeUrlLocal(String(src));
2151
+ const safe = sanitizeUrl(String(src));
1816
2152
  if (!safe) return "";
1817
2153
  return `<script src="${escapeAttrLocal(safe)}"></script>`;
1818
2154
  }).filter(Boolean).join("\n ");
@@ -1851,20 +2187,23 @@ function hydrateRouter(routes, options) {
1851
2187
  }
1852
2188
  createRouter(routes);
1853
2189
  const container = options?.container || document.getElementById("app");
1854
- if (container && serverState.path) {
1855
- const resolved = resolveServerRoute(serverState.path, routes);
2190
+ if (!container) return;
2191
+ const liveUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
2192
+ const resolved = resolveServerRoute(liveUrl, routes);
2193
+ const bootstrapEpoch = __getNavigationEpoch();
2194
+ import("./ssr-JFP5535P.js").then(async ({ hydrate }) => {
2195
+ const { replaceChildrenSafely } = await import("./dispose-4FZ6OFXR.js");
2196
+ if (__getNavigationEpoch() !== bootstrapEpoch) return;
1856
2197
  if (resolved.component) {
1857
- import("./ssr-2PPULEK2.js").then(({ hydrate }) => {
1858
- if (resolved.component) {
1859
- hydrate(resolved.component, container);
1860
- }
1861
- }).catch((err) => {
1862
- if (typeof console !== "undefined") {
1863
- console.error("[SibuJS routerSSR] failed to load hydrate:", err);
1864
- }
1865
- });
2198
+ hydrate(resolved.component, container);
2199
+ return;
1866
2200
  }
1867
- }
2201
+ replaceChildrenSafely(container);
2202
+ }).catch((err) => {
2203
+ if (typeof console !== "undefined") {
2204
+ console.error("[SibuJS routerSSR] failed to load hydrate:", err);
2205
+ }
2206
+ });
1868
2207
  }
1869
2208
  function createSSRRouter(routes) {
1870
2209
  return {
@@ -1879,54 +2218,6 @@ function createSSRRouter(routes) {
1879
2218
  }
1880
2219
  };
1881
2220
  }
1882
- var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
1883
- function isSafeAttrName(name) {
1884
- return SAFE_ATTR_NAME.test(name);
1885
- }
1886
- function isEventHandlerAttr(name) {
1887
- if (name.length < 3) return false;
1888
- const lower = name.toLowerCase();
1889
- return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
1890
- }
1891
- var URL_ATTRS = /* @__PURE__ */ new Set([
1892
- "href",
1893
- "src",
1894
- "action",
1895
- "formaction",
1896
- "cite",
1897
- "poster",
1898
- "background",
1899
- "srcset",
1900
- "ping",
1901
- "manifest",
1902
- "data",
1903
- "xlink:href"
1904
- ]);
1905
- function sanitizeUrlLocal(url) {
1906
- const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
1907
- if (!trimmed) return "";
1908
- const lower = trimmed.toLowerCase();
1909
- if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
1910
- return "";
1911
- }
1912
- return trimmed;
1913
- }
1914
- function buildSafeAttrString(attrs) {
1915
- const out = [];
1916
- for (const rawKey of Object.keys(attrs)) {
1917
- if (!Object.hasOwn(attrs, rawKey)) continue;
1918
- if (!isSafeAttrName(rawKey)) continue;
1919
- if (isEventHandlerAttr(rawKey)) continue;
1920
- const lowerKey = rawKey.toLowerCase();
1921
- let value = String(attrs[rawKey]);
1922
- if (URL_ATTRS.has(lowerKey)) {
1923
- value = sanitizeUrlLocal(value);
1924
- if (!value) continue;
1925
- }
1926
- out.push(`${rawKey}="${escapeAttrLocal(value)}"`);
1927
- }
1928
- return out.join(" ");
1929
- }
1930
2221
  function escapeHtmlLocal(str) {
1931
2222
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1932
2223
  }
@@ -1942,6 +2233,7 @@ export {
1942
2233
  Suspense,
1943
2234
  Trans,
1944
2235
  VERSION,
2236
+ __getNavigationEpoch,
1945
2237
  __removeRouterPagehideHandler,
1946
2238
  addRoute,
1947
2239
  afterEach,