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.
- package/LICENSE +21 -21
- package/README.md +118 -15
- package/dist/browser.cjs +410 -101
- package/dist/browser.d.cts +50 -5
- package/dist/browser.d.ts +50 -5
- package/dist/browser.js +8 -6
- package/dist/build.cjs +777 -385
- package/dist/build.js +15 -15
- package/dist/cdn.global.js +11 -7
- package/dist/{chunk-JCO7AHNU.js → chunk-3EKJK5FZ.js} +73 -34
- package/dist/chunk-4MFZMLUU.js +147 -0
- package/dist/chunk-52XFPGSN.js +123 -0
- package/dist/chunk-5INI7D2L.js +194 -0
- package/dist/{chunk-OAVXAFAY.js → chunk-6LG2T7LO.js} +5 -5
- package/dist/{chunk-OOUQUCNN.js → chunk-ACBT6UZS.js} +57 -22
- package/dist/{chunk-F7HW2NI5.js → chunk-AW6HPJ3L.js} +1 -1
- package/dist/chunk-DHDZ7IVN.js +109 -0
- package/dist/{chunk-JNGGM2XE.js → chunk-DQVUBVNX.js} +16 -25
- package/dist/{chunk-AXFWGY32.js → chunk-DR7H6UGM.js} +232 -67
- package/dist/{chunk-WSGLJYFI.js → chunk-E62UADKL.js} +9 -3
- package/dist/{chunk-3Z2NGWQI.js → chunk-ES2GERY2.js} +1 -1
- package/dist/{chunk-S3V6ZXPI.js → chunk-EWEE7NBF.js} +115 -41
- package/dist/{chunk-3DVGCW7O.js → chunk-F64ZPCFE.js} +8 -8
- package/dist/{chunk-APFRCIRI.js → chunk-GCSGGA3Q.js} +295 -198
- package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
- package/dist/{chunk-ELXMRSVN.js → chunk-NF3LYCQH.js} +259 -124
- package/dist/chunk-O3QQ3XOL.js +61 -0
- package/dist/{chunk-BUKYCWHX.js → chunk-PBQEW5VW.js} +14 -10
- package/dist/{chunk-H5FI5WDP.js → chunk-RRIDJQ5C.js} +3 -3
- package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
- package/dist/{chunk-ODXVQ3CS.js → chunk-VFHIVEPF.js} +309 -188
- package/dist/chunk-VPP2FONR.js +142 -0
- package/dist/{chunk-KZBEVFXZ.js → chunk-W55YHPEP.js} +5 -3
- package/dist/{chunk-FKQG7FTY.js → chunk-WN6WDFCB.js} +4 -4
- package/dist/{chunk-CEBWDD63.js → chunk-WOOMUJFT.js} +228 -86
- package/dist/{chunk-ITWCTZYH.js → chunk-Z2CE42DB.js} +19 -25
- package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.cts} +6 -6
- package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.ts} +6 -6
- package/dist/{customElement-CPfIrbvg.d.ts → customElement-CKLoO63w.d.cts} +73 -11
- package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.ts} +73 -11
- package/dist/data.cjs +483 -205
- package/dist/data.d.cts +139 -7
- package/dist/data.d.ts +139 -7
- package/dist/data.js +9 -9
- package/dist/devtools.cjs +222 -87
- package/dist/devtools.js +8 -8
- package/dist/dispose-4FZ6OFXR.js +19 -0
- package/dist/ecosystem.cjs +419 -175
- package/dist/ecosystem.js +11 -11
- package/dist/extras.cjs +1981 -765
- package/dist/extras.d.cts +7 -7
- package/dist/extras.d.ts +7 -7
- package/dist/extras.js +29 -26
- package/dist/index.cjs +784 -385
- package/dist/index.d.cts +299 -28
- package/dist/index.d.ts +299 -28
- package/dist/index.js +33 -18
- package/dist/motion.cjs +180 -50
- package/dist/motion.d.cts +26 -0
- package/dist/motion.d.ts +26 -0
- package/dist/motion.js +5 -5
- package/dist/patterns.cjs +437 -170
- package/dist/patterns.d.cts +60 -13
- package/dist/patterns.d.ts +60 -13
- package/dist/patterns.js +7 -7
- package/dist/performance.cjs +333 -148
- package/dist/performance.d.cts +55 -7
- package/dist/performance.d.ts +55 -7
- package/dist/performance.js +9 -9
- package/dist/plugins.cjs +1374 -576
- package/dist/plugins.d.cts +227 -9
- package/dist/plugins.d.ts +227 -9
- package/dist/plugins.js +613 -321
- package/dist/{ssr-D62yFwuw.d.cts → ssr-BiPRdZ6n.d.cts} +44 -15
- package/dist/{ssr-D62yFwuw.d.ts → ssr-BiPRdZ6n.d.ts} +44 -15
- package/dist/{ssr-2PPULEK2.js → ssr-JFP5535P.js} +5 -4
- package/dist/ssr.cjs +1104 -443
- package/dist/ssr.d.cts +230 -20
- package/dist/ssr.d.ts +230 -20
- package/dist/ssr.js +15 -12
- package/dist/{startup-0Qv6aosO.d.cts → startup-BMpaiMhP.d.cts} +1 -1
- package/dist/{startup-0Qv6aosO.d.ts → startup-BMpaiMhP.d.ts} +1 -1
- package/dist/testing.cjs +205 -63
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +8 -10
- package/dist/ui.cjs +640 -199
- package/dist/ui.d.cts +2 -2
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +10 -10
- package/dist/widgets.cjs +207 -82
- package/dist/widgets.js +9 -9
- package/package.json +12 -7
- package/dist/chunk-5VGSK6D2.js +0 -80
- package/dist/chunk-COY6PUD2.js +0 -34
- package/dist/chunk-K35BAR4P.js +0 -96
- package/dist/chunk-L3GAGWCC.js +0 -111
- 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-
|
|
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-
|
|
38
|
+
} from "./chunk-ES2GERY2.js";
|
|
39
39
|
import {
|
|
40
40
|
escapeScriptJson,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} from "./chunk-
|
|
44
|
-
import "./chunk-
|
|
45
|
-
import "./chunk-
|
|
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
|
-
|
|
48
|
+
sanitizeStyleAttribute,
|
|
52
49
|
sanitizeUrl,
|
|
53
50
|
stripControlChars
|
|
54
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-5INI7D2L.js";
|
|
52
|
+
import {
|
|
53
|
+
globalSingleton
|
|
54
|
+
} from "./chunk-7ZHH77QA.js";
|
|
55
55
|
import {
|
|
56
56
|
effect
|
|
57
|
-
} from "./chunk-
|
|
58
|
-
import
|
|
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-
|
|
64
|
+
} from "./chunk-DHDZ7IVN.js";
|
|
63
65
|
import {
|
|
64
66
|
signal
|
|
65
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-W55YHPEP.js";
|
|
66
68
|
import {
|
|
67
69
|
track
|
|
68
|
-
} from "./chunk-
|
|
69
|
-
import
|
|
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 [
|
|
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
|
-
|
|
93
|
+
const request = getRequestStore();
|
|
94
|
+
if (request) {
|
|
95
|
+
request.locale = locale;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setClientLocale(locale);
|
|
80
99
|
}
|
|
81
100
|
function getLocale() {
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
105
|
-
if (path === "") return
|
|
132
|
+
function classifyNavigationTarget(path) {
|
|
133
|
+
if (path === "") return "internal";
|
|
106
134
|
const normalized = stripControlChars(path).replace(/\\/g, "/");
|
|
107
|
-
if (normalized
|
|
108
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
490
|
-
|
|
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
|
|
497
|
-
this.
|
|
555
|
+
const plan = await loadingPromise;
|
|
556
|
+
this.planCache.set(cacheKey, plan);
|
|
498
557
|
this.errorCache.delete(cacheKey);
|
|
499
|
-
return
|
|
558
|
+
return plan;
|
|
500
559
|
} catch (error) {
|
|
501
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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.
|
|
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.
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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" &&
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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 (!
|
|
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
|
-
|
|
1023
|
+
h.replaceState(options.state || {}, "", fullPath);
|
|
786
1024
|
} else {
|
|
787
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
802
|
-
|
|
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.
|
|
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
|
|
1114
|
-
if (seq
|
|
1115
|
-
const node =
|
|
1116
|
-
|
|
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
|
-
|
|
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
|
|
1171
|
-
let
|
|
1477
|
+
let updateSeq = 0;
|
|
1478
|
+
let kaTorn = false;
|
|
1172
1479
|
const update = async () => {
|
|
1173
|
-
if (!_routerRef.current) return;
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1525
|
+
if (!fromCache) dispose(node);
|
|
1200
1526
|
return;
|
|
1201
1527
|
}
|
|
1202
|
-
|
|
1203
|
-
if (currentNode
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
|
1291
|
-
const
|
|
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
|
|
1308
|
-
const
|
|
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,
|
|
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
|
-
|
|
1371
|
-
|
|
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 ||
|
|
1698
|
+
const showFallback = (myGeneration) => {
|
|
1699
|
+
if (fallbackNode || !props.fallback || tornDown) return;
|
|
1700
|
+
if (!anchor.parentNode) return;
|
|
1701
|
+
let fallback;
|
|
1380
1702
|
try {
|
|
1381
|
-
|
|
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
|
-
|
|
1391
|
-
if (
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1411
|
-
cleanupNodes();
|
|
1412
|
-
anchor.parentNode.insertBefore(result, anchor.nextSibling);
|
|
1413
|
-
currentNode = result;
|
|
1414
|
-
}
|
|
1728
|
+
element = result;
|
|
1415
1729
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1517
|
-
if (seq
|
|
1518
|
-
const node =
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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 || []).
|
|
1807
|
-
|
|
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 =
|
|
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
|
|
1855
|
-
|
|
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
|
-
|
|
1858
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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,
|