sibujs 3.4.1 → 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-WEZ5SP7L.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-5MFF5EPT.js → chunk-6LG2T7LO.js} +5 -5
- package/dist/{chunk-BQLVTAUZ.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-NXR2YFZB.js → chunk-DQVUBVNX.js} +16 -25
- package/dist/{chunk-7PNKW3FJ.js → chunk-DR7H6UGM.js} +232 -67
- package/dist/{chunk-CV7FAQ2R.js → chunk-E62UADKL.js} +9 -3
- package/dist/{chunk-VQZDHAEK.js → chunk-ES2GERY2.js} +1 -1
- package/dist/{chunk-FWFEBYQ6.js → chunk-EWEE7NBF.js} +115 -41
- package/dist/{chunk-PBZWJQNY.js → chunk-F64ZPCFE.js} +8 -8
- package/dist/{chunk-S2VW4NBA.js → chunk-GCSGGA3Q.js} +295 -198
- package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
- package/dist/{chunk-MPE75KOA.js → chunk-NF3LYCQH.js} +259 -124
- package/dist/chunk-O3QQ3XOL.js +61 -0
- package/dist/{chunk-X4JS367G.js → chunk-PBQEW5VW.js} +14 -10
- package/dist/{chunk-QHPUFNHZ.js → chunk-RRIDJQ5C.js} +3 -3
- package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
- package/dist/{chunk-3FFCUIGV.js → chunk-VFHIVEPF.js} +309 -188
- package/dist/chunk-VPP2FONR.js +142 -0
- package/dist/{chunk-335KJEN2.js → chunk-W55YHPEP.js} +5 -3
- package/dist/{chunk-Y22QAKJX.js → chunk-WN6WDFCB.js} +4 -4
- package/dist/{chunk-YDWP3ZOU.js → chunk-WOOMUJFT.js} +228 -86
- package/dist/{chunk-4I3M326N.js → chunk-Z2CE42DB.js} +19 -25
- package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.cts} +6 -6
- package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.ts} +6 -6
- package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.cts} +73 -11
- package/dist/{customElement-CPfIrbvg.d.ts → 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 +1371 -576
- package/dist/plugins.d.cts +227 -9
- package/dist/plugins.d.ts +227 -9
- package/dist/plugins.js +610 -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-6CAJU5TP.js +0 -38
- package/dist/chunk-COY6PUD2.js +0 -34
- package/dist/chunk-DLBFEAIS.js +0 -96
- package/dist/chunk-L3GAGWCC.js +0 -111
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: "",
|
|
@@ -607,7 +813,7 @@ var _SibuRouter = class _SibuRouter {
|
|
|
607
813
|
}
|
|
608
814
|
queueMicrotask(() => {
|
|
609
815
|
if (!this.navigator.isNavigating) {
|
|
610
|
-
this.handleLocationChange(true);
|
|
816
|
+
this.handleLocationChange(true, true);
|
|
611
817
|
}
|
|
612
818
|
this.isReadySetter(true);
|
|
613
819
|
});
|
|
@@ -622,14 +828,15 @@ var _SibuRouter = class _SibuRouter {
|
|
|
622
828
|
matched: []
|
|
623
829
|
};
|
|
624
830
|
}
|
|
625
|
-
handleLocationChange(skipHistory = true) {
|
|
831
|
+
handleLocationChange(skipHistory = true, initialResolution = false) {
|
|
626
832
|
const path = this.getCurrentPath();
|
|
627
|
-
this.navigate(path, { replace: true, skipHistory }).catch((err) => {
|
|
833
|
+
this.navigate(path, { replace: true, skipHistory, initialResolution }).catch((err) => {
|
|
628
834
|
console.error("[Router] Error during location change navigation:", err);
|
|
629
835
|
});
|
|
630
836
|
}
|
|
631
837
|
getCurrentPath() {
|
|
632
838
|
const { mode, base } = this.options;
|
|
839
|
+
if (typeof window === "undefined") return "/";
|
|
633
840
|
if (mode === "hash") {
|
|
634
841
|
return window.location.hash.slice(1) || "/";
|
|
635
842
|
}
|
|
@@ -661,15 +868,15 @@ var _SibuRouter = class _SibuRouter {
|
|
|
661
868
|
try {
|
|
662
869
|
await this.navigator.navigate(async (signal2) => {
|
|
663
870
|
const targetPath = this.resolvePath(to);
|
|
664
|
-
if (!
|
|
871
|
+
if (!isRouterNavigable(targetPath)) {
|
|
665
872
|
const from2 = this.currentRouteGetter();
|
|
666
873
|
const toContext2 = this.createRouteContext(targetPath);
|
|
667
|
-
throw new NavigationFailureError("aborted", from2, toContext2);
|
|
874
|
+
throw new NavigationFailureError("aborted", from2, toContext2, void 0, "unsafe-target");
|
|
668
875
|
}
|
|
669
876
|
const from = this.currentRouteGetter();
|
|
670
877
|
const toContext = this.createRouteContext(targetPath);
|
|
671
878
|
if (this.isSameRoute(from, toContext)) {
|
|
672
|
-
throw new NavigationFailureError("duplicated", from, toContext);
|
|
879
|
+
throw new NavigationFailureError("duplicated", from, toContext, void 0, "duplicate");
|
|
673
880
|
}
|
|
674
881
|
await this.performNavigation(toContext, from, options, signal2);
|
|
675
882
|
});
|
|
@@ -677,30 +884,43 @@ var _SibuRouter = class _SibuRouter {
|
|
|
677
884
|
} catch (error) {
|
|
678
885
|
if (error instanceof NavigationFailureError) {
|
|
679
886
|
const failure2 = error.toFailure();
|
|
680
|
-
return { success: false, type: failure2.type, failure: failure2 };
|
|
887
|
+
return { success: false, type: failure2.type, reason: failure2.reason, failure: failure2 };
|
|
681
888
|
}
|
|
889
|
+
const isAbort = error instanceof Error && error.message === "Navigation aborted";
|
|
890
|
+
const reason = isAbort ? this.destroyed ? "router-destroyed" : "superseded" : "error";
|
|
682
891
|
const failure = {
|
|
683
892
|
type: "aborted",
|
|
893
|
+
reason,
|
|
684
894
|
from: this.currentRouteGetter(),
|
|
685
895
|
to: this.createRouteContext(this.resolvePath(to)),
|
|
686
896
|
error: error instanceof Error ? error : new Error(String(error))
|
|
687
897
|
};
|
|
688
|
-
return { success: false, type: failure.type, failure };
|
|
898
|
+
return { success: false, type: failure.type, reason, failure };
|
|
689
899
|
}
|
|
690
900
|
}
|
|
691
|
-
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;
|
|
692
903
|
if (depth >= _SibuRouter.MAX_REDIRECT_DEPTH) {
|
|
693
|
-
|
|
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");
|
|
694
911
|
}
|
|
695
912
|
const beforeEachResult = await this.guards.runBeforeEach(to, from, signal2);
|
|
696
913
|
if (beforeEachResult !== true) {
|
|
697
914
|
if (typeof beforeEachResult === "string") {
|
|
698
|
-
if (!
|
|
699
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
915
|
+
if (!isRouterNavigable(beforeEachResult)) {
|
|
916
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
700
917
|
}
|
|
701
|
-
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
|
+
]);
|
|
702
922
|
}
|
|
703
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
923
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "guard");
|
|
704
924
|
}
|
|
705
925
|
const match = this.matcher.match(to.path);
|
|
706
926
|
if (match) {
|
|
@@ -711,47 +931,60 @@ var _SibuRouter = class _SibuRouter {
|
|
|
711
931
|
for (const guard of guards) {
|
|
712
932
|
if (signal2.aborted) throw new Error("Navigation aborted");
|
|
713
933
|
const result = await guard(to, from);
|
|
934
|
+
if (signal2.aborted) {
|
|
935
|
+
throw new NavigationFailureError("aborted", from, to, void 0, abortReasonOf(signal2));
|
|
936
|
+
}
|
|
714
937
|
if (result !== true) {
|
|
715
938
|
if (typeof result === "string") {
|
|
716
|
-
if (!
|
|
717
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
939
|
+
if (!isRouterNavigable(result)) {
|
|
940
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
718
941
|
}
|
|
719
|
-
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
|
+
]);
|
|
720
946
|
}
|
|
721
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
947
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "guard");
|
|
722
948
|
}
|
|
723
949
|
}
|
|
724
950
|
}
|
|
725
951
|
}
|
|
726
952
|
if ("redirect" in route2) {
|
|
727
953
|
const redirectPath = typeof route2.redirect === "function" ? route2.redirect(to) : route2.redirect;
|
|
728
|
-
if (typeof redirectPath === "string" &&
|
|
729
|
-
if (typeof console !== "undefined") {
|
|
954
|
+
if (typeof redirectPath === "string" && !isRouterNavigable(redirectPath)) {
|
|
955
|
+
if (classifyNavigationTarget(redirectPath) === "external" && typeof console !== "undefined") {
|
|
730
956
|
console.error(
|
|
731
957
|
`[SibuJS Router] Refusing absolute/protocol-relative redirect "${redirectPath}" \u2014 open-redirect risk.`
|
|
732
958
|
);
|
|
733
959
|
}
|
|
734
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
735
|
-
}
|
|
736
|
-
if (typeof redirectPath === "string" && !isSafeNavigationTarget(redirectPath)) {
|
|
737
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
960
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
738
961
|
}
|
|
739
|
-
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
|
+
]);
|
|
740
966
|
}
|
|
741
967
|
}
|
|
742
968
|
const beforeResolveResult = await this.guards.runBeforeResolve(to, from, signal2);
|
|
743
969
|
if (beforeResolveResult !== true) {
|
|
744
970
|
if (typeof beforeResolveResult === "string") {
|
|
745
|
-
if (!
|
|
746
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
971
|
+
if (!isRouterNavigable(beforeResolveResult)) {
|
|
972
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
747
973
|
}
|
|
748
|
-
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
|
+
]);
|
|
749
978
|
}
|
|
750
|
-
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));
|
|
751
983
|
}
|
|
752
984
|
if (!options.skipHistory) {
|
|
753
985
|
this.updateHistory(to, options);
|
|
754
986
|
}
|
|
987
|
+
if (!options.initialResolution) this.navEpoch++;
|
|
755
988
|
this.currentRouteSetter(to);
|
|
756
989
|
this.guards.runAfterEach(to, from);
|
|
757
990
|
this.handleScrollBehavior(to, from);
|
|
@@ -784,25 +1017,71 @@ var _SibuRouter = class _SibuRouter {
|
|
|
784
1017
|
}
|
|
785
1018
|
updateHistory(to, options) {
|
|
786
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;
|
|
787
1022
|
if (options.replace) {
|
|
788
|
-
|
|
1023
|
+
h.replaceState(options.state || {}, "", fullPath);
|
|
789
1024
|
} else {
|
|
790
|
-
|
|
1025
|
+
h.pushState(options.state || {}, "", fullPath);
|
|
791
1026
|
}
|
|
792
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
|
+
*/
|
|
793
1047
|
handleScrollBehavior(to, from) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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;
|
|
801
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
|
+
});
|
|
802
1069
|
}
|
|
803
1070
|
// Component loading
|
|
804
|
-
|
|
805
|
-
|
|
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);
|
|
806
1085
|
}
|
|
807
1086
|
// Guards API
|
|
808
1087
|
beforeEach(guard) {
|
|
@@ -837,12 +1116,18 @@ var _SibuRouter = class _SibuRouter {
|
|
|
837
1116
|
get isReady() {
|
|
838
1117
|
return this.isReadyGetter();
|
|
839
1118
|
}
|
|
1119
|
+
/** @internal Navigation generation — see `navEpoch`. Not a public API. */
|
|
1120
|
+
get navigationEpoch() {
|
|
1121
|
+
return this.navEpoch;
|
|
1122
|
+
}
|
|
840
1123
|
get isNavigating() {
|
|
841
1124
|
return this.navigator.isNavigating;
|
|
842
1125
|
}
|
|
843
1126
|
// Cleanup
|
|
844
1127
|
destroy() {
|
|
845
|
-
this.
|
|
1128
|
+
this.destroyed = true;
|
|
1129
|
+
this.navEpoch++;
|
|
1130
|
+
this.navigator.abort(ABORT_ROUTER_DESTROYED);
|
|
846
1131
|
for (const fn of this.cleanup) fn();
|
|
847
1132
|
this.cleanup = [];
|
|
848
1133
|
this.guards.clear();
|
|
@@ -885,10 +1170,11 @@ var _SibuRouter = class _SibuRouter {
|
|
|
885
1170
|
_SibuRouter.MAX_REDIRECT_DEPTH = 10;
|
|
886
1171
|
var SibuRouter = _SibuRouter;
|
|
887
1172
|
var NavigationFailureError = class extends Error {
|
|
888
|
-
constructor(type, from, to, error) {
|
|
889
|
-
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}`);
|
|
890
1175
|
this.name = "NavigationFailureError";
|
|
891
1176
|
this.type = type;
|
|
1177
|
+
this.reason = reason;
|
|
892
1178
|
this.from = from;
|
|
893
1179
|
this.to = to;
|
|
894
1180
|
if (error) {
|
|
@@ -898,12 +1184,16 @@ var NavigationFailureError = class extends Error {
|
|
|
898
1184
|
toFailure() {
|
|
899
1185
|
return {
|
|
900
1186
|
type: this.type,
|
|
1187
|
+
reason: this.reason,
|
|
901
1188
|
from: this.from,
|
|
902
1189
|
to: this.to,
|
|
903
1190
|
error: this.cause instanceof Error ? this.cause : void 0
|
|
904
1191
|
};
|
|
905
1192
|
}
|
|
906
1193
|
};
|
|
1194
|
+
function abortReasonOf(signal2) {
|
|
1195
|
+
return signal2.reason === ABORT_ROUTER_DESTROYED ? "router-destroyed" : "superseded";
|
|
1196
|
+
}
|
|
907
1197
|
var ROUTER_KEY = /* @__PURE__ */ Symbol.for("sibujs.router.v1");
|
|
908
1198
|
var _routerRef = globalThis[ROUTER_KEY] ?? (globalThis[ROUTER_KEY] = { current: null });
|
|
909
1199
|
function normalizeRoutes(routes) {
|
|
@@ -1007,6 +1297,13 @@ function Route() {
|
|
|
1007
1297
|
let errorNode = null;
|
|
1008
1298
|
let navSeq = 0;
|
|
1009
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
|
+
};
|
|
1010
1307
|
const cleanupNodes = () => {
|
|
1011
1308
|
[currentNode, loadingNode, errorNode].forEach((node) => {
|
|
1012
1309
|
if (!node) return;
|
|
@@ -1084,7 +1381,7 @@ function Route() {
|
|
|
1084
1381
|
anchor.parentNode.insertBefore(errorNode, anchor.nextSibling);
|
|
1085
1382
|
};
|
|
1086
1383
|
const update = async () => {
|
|
1087
|
-
if (!_routerRef.current) return;
|
|
1384
|
+
if (routeTorn || !_routerRef.current) return;
|
|
1088
1385
|
const seq = ++navSeq;
|
|
1089
1386
|
const route2 = _routerRef.current.currentRoute;
|
|
1090
1387
|
try {
|
|
@@ -1113,17 +1410,22 @@ function Route() {
|
|
|
1113
1410
|
if (isAsync) {
|
|
1114
1411
|
showLoading();
|
|
1115
1412
|
}
|
|
1116
|
-
const
|
|
1117
|
-
if (seq
|
|
1118
|
-
const node =
|
|
1119
|
-
|
|
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) {
|
|
1120
1422
|
currentTopRoute = routeDef;
|
|
1121
1423
|
cleanupNodes();
|
|
1122
|
-
|
|
1424
|
+
parent.insertBefore(node, anchor.nextSibling);
|
|
1123
1425
|
currentNode = node;
|
|
1124
1426
|
}
|
|
1125
1427
|
} catch (error) {
|
|
1126
|
-
if (seq !== navSeq) return;
|
|
1428
|
+
if (routeTorn || seq !== navSeq) return;
|
|
1127
1429
|
hideLoading();
|
|
1128
1430
|
console.error("[Route] Component error:", error);
|
|
1129
1431
|
showError(error instanceof Error ? error : new Error(String(error)), routeDef);
|
|
@@ -1131,6 +1433,7 @@ function Route() {
|
|
|
1131
1433
|
}
|
|
1132
1434
|
} catch (error) {
|
|
1133
1435
|
if (seq !== navSeq) return;
|
|
1436
|
+
if (routeTorn) return;
|
|
1134
1437
|
console.error("[Route] Update failed:", error);
|
|
1135
1438
|
showError(error instanceof Error ? error : new Error(String(error)));
|
|
1136
1439
|
}
|
|
@@ -1147,12 +1450,13 @@ function Route() {
|
|
|
1147
1450
|
if (!routeInitialized && anchor.parentNode) wrappedUpdate();
|
|
1148
1451
|
});
|
|
1149
1452
|
}
|
|
1150
|
-
let routeTorn = false;
|
|
1151
1453
|
const routeCleanup = () => {
|
|
1152
1454
|
if (routeTorn) return;
|
|
1153
1455
|
routeTorn = true;
|
|
1456
|
+
navSeq++;
|
|
1154
1457
|
routeTeardown();
|
|
1155
1458
|
cleanupNodes();
|
|
1459
|
+
currentTopRoute = null;
|
|
1156
1460
|
};
|
|
1157
1461
|
registerDisposer(anchor, routeCleanup);
|
|
1158
1462
|
routeCleanups.push(routeCleanup);
|
|
@@ -1170,16 +1474,14 @@ function KeepAliveRoute(options) {
|
|
|
1170
1474
|
let currentNode = null;
|
|
1171
1475
|
let currentKey = "";
|
|
1172
1476
|
let currentCached = false;
|
|
1173
|
-
let
|
|
1174
|
-
let
|
|
1477
|
+
let updateSeq = 0;
|
|
1478
|
+
let kaTorn = false;
|
|
1175
1479
|
const update = async () => {
|
|
1176
|
-
if (!_routerRef.current) return;
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const route2 = _routerRef.current.currentRoute;
|
|
1182
|
-
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);
|
|
1183
1485
|
if (!match) return;
|
|
1184
1486
|
const { route: routeDef } = match;
|
|
1185
1487
|
if ("redirect" in routeDef) {
|
|
@@ -1196,63 +1498,62 @@ function KeepAliveRoute(options) {
|
|
|
1196
1498
|
const cacheKey = `${route2.path}${queryStr}${route2.hash ? `#${route2.hash}` : ""}`;
|
|
1197
1499
|
const shouldCache = !includeNames || routeDef.name != null && includeNames.includes(routeDef.name);
|
|
1198
1500
|
if (cacheKey === currentKey && currentNode) return;
|
|
1199
|
-
|
|
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
|
+
}
|
|
1200
1523
|
const parent = anchor.parentNode;
|
|
1201
1524
|
if (!parent) {
|
|
1202
|
-
|
|
1525
|
+
if (!fromCache) dispose(node);
|
|
1203
1526
|
return;
|
|
1204
1527
|
}
|
|
1205
|
-
|
|
1206
|
-
if (currentNode
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const component = await _routerRef.current.loadComponent(routeDef, route2.path);
|
|
1222
|
-
const node = component();
|
|
1223
|
-
if (!node || route2.path !== _routerRef.current.currentRoute.path) {
|
|
1224
|
-
isUpdating = false;
|
|
1225
|
-
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;
|
|
1226
1544
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
while (lruOrder.length > maxCache) {
|
|
1233
|
-
const evictKey = lruOrder.shift();
|
|
1234
|
-
const evictNode = cache.get(evictKey);
|
|
1235
|
-
if (evictNode) {
|
|
1236
|
-
dispose(evictNode);
|
|
1237
|
-
if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
|
|
1238
|
-
cache.delete(evictKey);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1545
|
+
const evictNode = cache.get(evictKey);
|
|
1546
|
+
if (evictNode) {
|
|
1547
|
+
dispose(evictNode);
|
|
1548
|
+
if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
|
|
1549
|
+
cache.delete(evictKey);
|
|
1241
1550
|
}
|
|
1242
1551
|
}
|
|
1243
|
-
currentKey = cacheKey;
|
|
1244
|
-
if (currentNode) {
|
|
1245
|
-
parent.insertBefore(currentNode, anchor.nextSibling);
|
|
1246
|
-
}
|
|
1247
|
-
} catch (error) {
|
|
1248
|
-
console.error("[KeepAliveRoute] Component error:", error);
|
|
1249
|
-
} finally {
|
|
1250
|
-
isUpdating = false;
|
|
1251
|
-
if (pendingUpdate) {
|
|
1252
|
-
pendingUpdate = false;
|
|
1253
|
-
update();
|
|
1254
|
-
}
|
|
1255
1552
|
}
|
|
1553
|
+
currentNode = node;
|
|
1554
|
+
currentCached = fromCache || shouldCache;
|
|
1555
|
+
currentKey = cacheKey;
|
|
1556
|
+
parent.insertBefore(node, anchor.nextSibling);
|
|
1256
1557
|
};
|
|
1257
1558
|
let initialized = false;
|
|
1258
1559
|
const wrappedUpdate = async () => {
|
|
@@ -1265,10 +1566,10 @@ function KeepAliveRoute(options) {
|
|
|
1265
1566
|
if (!initialized && anchor.parentNode) wrappedUpdate();
|
|
1266
1567
|
});
|
|
1267
1568
|
}
|
|
1268
|
-
let kaTorn = false;
|
|
1269
1569
|
const kaCleanup = () => {
|
|
1270
1570
|
if (kaTorn) return;
|
|
1271
1571
|
kaTorn = true;
|
|
1572
|
+
updateSeq++;
|
|
1272
1573
|
kaTeardown();
|
|
1273
1574
|
for (const node of cache.values()) {
|
|
1274
1575
|
dispose(node);
|
|
@@ -1277,7 +1578,10 @@ function KeepAliveRoute(options) {
|
|
|
1277
1578
|
cache.clear();
|
|
1278
1579
|
lruOrder.length = 0;
|
|
1279
1580
|
if (currentNode?.parentNode) currentNode.parentNode.removeChild(currentNode);
|
|
1581
|
+
if (currentNode && !currentCached) dispose(currentNode);
|
|
1280
1582
|
currentNode = null;
|
|
1583
|
+
currentKey = "";
|
|
1584
|
+
currentCached = false;
|
|
1281
1585
|
};
|
|
1282
1586
|
registerDisposer(anchor, kaCleanup);
|
|
1283
1587
|
routeCleanups.push(kaCleanup);
|
|
@@ -1290,8 +1594,12 @@ function RouterLink(props, children) {
|
|
|
1290
1594
|
const baseClass = typeof classAttr === "string" ? classAttr : "";
|
|
1291
1595
|
const routeGetter = _routerRef.current.routeGetter;
|
|
1292
1596
|
const rawHref = _routerRef.current["resolvePath"](to);
|
|
1293
|
-
const
|
|
1294
|
-
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);
|
|
1295
1603
|
const link = document.createElement("a");
|
|
1296
1604
|
link.href = href;
|
|
1297
1605
|
if (target) {
|
|
@@ -1307,8 +1615,9 @@ function RouterLink(props, children) {
|
|
|
1307
1615
|
const options = _routerRef.current["options"];
|
|
1308
1616
|
const effectCleanup = effect(() => {
|
|
1309
1617
|
const route2 = routeGetter();
|
|
1310
|
-
const
|
|
1311
|
-
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;
|
|
1312
1621
|
const classes = [];
|
|
1313
1622
|
if (isActive) {
|
|
1314
1623
|
if (activeClass) classes.push(activeClass);
|
|
@@ -1330,7 +1639,7 @@ function RouterLink(props, children) {
|
|
|
1330
1639
|
const safe = sanitizeUrl(str);
|
|
1331
1640
|
if (safe) link.setAttribute(key, safe);
|
|
1332
1641
|
} else if (lkey === "style") {
|
|
1333
|
-
link.setAttribute(key,
|
|
1642
|
+
link.setAttribute(key, sanitizeStyleAttribute(str));
|
|
1334
1643
|
} else {
|
|
1335
1644
|
link.setAttribute(key, str);
|
|
1336
1645
|
}
|
|
@@ -1350,10 +1659,13 @@ function RouterLink(props, children) {
|
|
|
1350
1659
|
});
|
|
1351
1660
|
}
|
|
1352
1661
|
const onLinkClick = (e) => {
|
|
1353
|
-
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) {
|
|
1354
1663
|
return;
|
|
1355
1664
|
}
|
|
1665
|
+
if (link.hasAttribute("download")) return;
|
|
1666
|
+
if (kind === "external") return;
|
|
1356
1667
|
e.preventDefault();
|
|
1668
|
+
if (kind === "unsafe") return;
|
|
1357
1669
|
_routerRef.current?.navigate(to, { replace: replace2 }).catch((err) => {
|
|
1358
1670
|
if (typeof console !== "undefined") console.error("[router] link navigate failed:", err);
|
|
1359
1671
|
});
|
|
@@ -1369,68 +1681,80 @@ function Suspense(props) {
|
|
|
1369
1681
|
let currentNode = null;
|
|
1370
1682
|
let fallbackNode = null;
|
|
1371
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;
|
|
1372
1692
|
const cleanupNodes = () => {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
node.parentNode.removeChild(node);
|
|
1376
|
-
}
|
|
1377
|
-
});
|
|
1693
|
+
release(currentNode);
|
|
1694
|
+
release(fallbackNode);
|
|
1378
1695
|
currentNode = null;
|
|
1379
1696
|
fallbackNode = null;
|
|
1380
1697
|
};
|
|
1381
|
-
const showFallback = () => {
|
|
1382
|
-
if (fallbackNode || !props.fallback ||
|
|
1698
|
+
const showFallback = (myGeneration) => {
|
|
1699
|
+
if (fallbackNode || !props.fallback || tornDown) return;
|
|
1700
|
+
if (!anchor.parentNode) return;
|
|
1701
|
+
let fallback;
|
|
1383
1702
|
try {
|
|
1384
|
-
|
|
1385
|
-
if (fallback instanceof HTMLElement) {
|
|
1386
|
-
fallbackNode = fallback;
|
|
1387
|
-
anchor.parentNode.insertBefore(fallbackNode, anchor.nextSibling);
|
|
1388
|
-
}
|
|
1703
|
+
fallback = typeof props.fallback === "function" ? props.fallback() : props.fallback;
|
|
1389
1704
|
} catch (error) {
|
|
1390
1705
|
console.error("[Suspense] Fallback error:", error);
|
|
1706
|
+
return;
|
|
1391
1707
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1396
|
-
|
|
1708
|
+
if (!(fallback instanceof HTMLElement)) return;
|
|
1709
|
+
const parent = commitTarget(myGeneration);
|
|
1710
|
+
if (!parent) {
|
|
1711
|
+
release(fallback);
|
|
1712
|
+
return;
|
|
1397
1713
|
}
|
|
1714
|
+
fallbackNode = fallback;
|
|
1715
|
+
parent.insertBefore(fallbackNode, anchor.nextSibling);
|
|
1398
1716
|
};
|
|
1399
1717
|
const render = async () => {
|
|
1400
|
-
if (isLoading) return;
|
|
1718
|
+
if (tornDown || isLoading) return;
|
|
1401
1719
|
isLoading = true;
|
|
1720
|
+
const myGeneration = ++generation;
|
|
1402
1721
|
try {
|
|
1403
1722
|
const result = props.nodes();
|
|
1723
|
+
let element;
|
|
1404
1724
|
if (result instanceof Promise) {
|
|
1405
|
-
showFallback();
|
|
1406
|
-
|
|
1407
|
-
if (anchor.parentNode) {
|
|
1408
|
-
cleanupNodes();
|
|
1409
|
-
anchor.parentNode.insertBefore(element, anchor.nextSibling);
|
|
1410
|
-
currentNode = element;
|
|
1411
|
-
}
|
|
1725
|
+
showFallback(myGeneration);
|
|
1726
|
+
element = await result;
|
|
1412
1727
|
} else {
|
|
1413
|
-
|
|
1414
|
-
cleanupNodes();
|
|
1415
|
-
anchor.parentNode.insertBefore(result, anchor.nextSibling);
|
|
1416
|
-
currentNode = result;
|
|
1417
|
-
}
|
|
1728
|
+
element = result;
|
|
1418
1729
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
if (anchor.parentNode) {
|
|
1423
|
-
const errorElement = document.createElement("div");
|
|
1424
|
-
errorElement.className = "suspense-error";
|
|
1425
|
-
errorElement.textContent = error instanceof Error ? error.message : "Failed to load";
|
|
1730
|
+
const parent = commitTarget(myGeneration);
|
|
1731
|
+
if (!parent) {
|
|
1732
|
+
release(element);
|
|
1426
1733
|
cleanupNodes();
|
|
1427
|
-
|
|
1428
|
-
currentNode = errorElement;
|
|
1734
|
+
return;
|
|
1429
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;
|
|
1430
1749
|
} finally {
|
|
1431
1750
|
isLoading = false;
|
|
1432
1751
|
}
|
|
1433
1752
|
};
|
|
1753
|
+
registerDisposer(anchor, () => {
|
|
1754
|
+
tornDown = true;
|
|
1755
|
+
generation++;
|
|
1756
|
+
cleanupNodes();
|
|
1757
|
+
});
|
|
1434
1758
|
queueMicrotask(render);
|
|
1435
1759
|
return anchor;
|
|
1436
1760
|
}
|
|
@@ -1445,12 +1769,15 @@ async function preloadRoute(to) {
|
|
|
1445
1769
|
const match = _routerRef.current["matcher"].match(path.split("?")[0].split("#")[0]);
|
|
1446
1770
|
if (match && "component" in match.route) {
|
|
1447
1771
|
try {
|
|
1448
|
-
await _routerRef.current.
|
|
1772
|
+
await _routerRef.current.preloadPlan(match.route, path);
|
|
1449
1773
|
} catch (error) {
|
|
1450
1774
|
console.warn("[Router] Preload failed:", error);
|
|
1451
1775
|
}
|
|
1452
1776
|
}
|
|
1453
1777
|
}
|
|
1778
|
+
function __getNavigationEpoch() {
|
|
1779
|
+
return _routerRef.current?.navigationEpoch ?? -1;
|
|
1780
|
+
}
|
|
1454
1781
|
function hasRoute(name) {
|
|
1455
1782
|
if (!_routerRef.current) return false;
|
|
1456
1783
|
return _routerRef.current["matcher"].findByName(name) !== null;
|
|
@@ -1492,6 +1819,7 @@ function Outlet() {
|
|
|
1492
1819
|
let currentNode = null;
|
|
1493
1820
|
let currentChild = null;
|
|
1494
1821
|
let navSeq = 0;
|
|
1822
|
+
let outletTorn = false;
|
|
1495
1823
|
const clearCurrent = () => {
|
|
1496
1824
|
if (currentNode) {
|
|
1497
1825
|
dispose(currentNode);
|
|
@@ -1500,8 +1828,14 @@ function Outlet() {
|
|
|
1500
1828
|
}
|
|
1501
1829
|
currentChild = null;
|
|
1502
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
|
+
};
|
|
1503
1837
|
const update = async () => {
|
|
1504
|
-
if (!_routerRef.current) return;
|
|
1838
|
+
if (outletTorn || !_routerRef.current) return;
|
|
1505
1839
|
const seq = ++navSeq;
|
|
1506
1840
|
const route2 = _routerRef.current.currentRoute;
|
|
1507
1841
|
if (route2.matched.length < 2) {
|
|
@@ -1516,17 +1850,21 @@ function Outlet() {
|
|
|
1516
1850
|
if (childRoute === currentChild && currentNode) return;
|
|
1517
1851
|
try {
|
|
1518
1852
|
const cacheKey = `${route2.path}\0${childRoute.path}`;
|
|
1519
|
-
const
|
|
1520
|
-
if (seq
|
|
1521
|
-
const node =
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
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;
|
|
1527
1860
|
}
|
|
1861
|
+
if (!node) return;
|
|
1862
|
+
clearCurrent();
|
|
1863
|
+
parent.insertBefore(node, anchor.nextSibling);
|
|
1864
|
+
currentNode = node;
|
|
1865
|
+
currentChild = childRoute;
|
|
1528
1866
|
} catch (error) {
|
|
1529
|
-
if (seq !== navSeq) return;
|
|
1867
|
+
if (outletTorn || seq !== navSeq) return;
|
|
1530
1868
|
console.error("[Outlet] Failed to render child route:", error);
|
|
1531
1869
|
}
|
|
1532
1870
|
};
|
|
@@ -1536,16 +1874,17 @@ function Outlet() {
|
|
|
1536
1874
|
if (anchor.parentNode) update();
|
|
1537
1875
|
});
|
|
1538
1876
|
}
|
|
1539
|
-
let outletTorn = false;
|
|
1540
1877
|
const outletCleanup = () => {
|
|
1541
1878
|
if (outletTorn) return;
|
|
1542
1879
|
outletTorn = true;
|
|
1880
|
+
navSeq++;
|
|
1543
1881
|
outletTeardown();
|
|
1544
1882
|
if (currentNode) {
|
|
1545
1883
|
dispose(currentNode);
|
|
1546
1884
|
if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
|
|
1547
1885
|
currentNode = null;
|
|
1548
1886
|
}
|
|
1887
|
+
currentChild = null;
|
|
1549
1888
|
};
|
|
1550
1889
|
registerDisposer(anchor, outletCleanup);
|
|
1551
1890
|
routeCleanups.push(outletCleanup);
|
|
@@ -1806,16 +2145,10 @@ function renderRouteToString(url, routes, _options) {
|
|
|
1806
2145
|
function renderRouteToDocument(url, routes, options) {
|
|
1807
2146
|
const { html, state } = renderRouteToString(url, routes, options);
|
|
1808
2147
|
const opts = options || {};
|
|
1809
|
-
const metaTags = (opts.meta || []).
|
|
1810
|
-
|
|
1811
|
-
return pairs ? `<meta ${pairs} />` : "";
|
|
1812
|
-
}).filter(Boolean).join("\n ");
|
|
1813
|
-
const linkTags = (opts.links || []).map((attrs) => {
|
|
1814
|
-
const pairs = buildSafeAttrString(attrs);
|
|
1815
|
-
return pairs ? `<link ${pairs} />` : "";
|
|
1816
|
-
}).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 ");
|
|
1817
2150
|
const scriptTags = (opts.scripts || []).map((src) => {
|
|
1818
|
-
const safe =
|
|
2151
|
+
const safe = sanitizeUrl(String(src));
|
|
1819
2152
|
if (!safe) return "";
|
|
1820
2153
|
return `<script src="${escapeAttrLocal(safe)}"></script>`;
|
|
1821
2154
|
}).filter(Boolean).join("\n ");
|
|
@@ -1854,20 +2187,23 @@ function hydrateRouter(routes, options) {
|
|
|
1854
2187
|
}
|
|
1855
2188
|
createRouter(routes);
|
|
1856
2189
|
const container = options?.container || document.getElementById("app");
|
|
1857
|
-
if (container
|
|
1858
|
-
|
|
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;
|
|
1859
2197
|
if (resolved.component) {
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
hydrate(resolved.component, container);
|
|
1863
|
-
}
|
|
1864
|
-
}).catch((err) => {
|
|
1865
|
-
if (typeof console !== "undefined") {
|
|
1866
|
-
console.error("[SibuJS routerSSR] failed to load hydrate:", err);
|
|
1867
|
-
}
|
|
1868
|
-
});
|
|
2198
|
+
hydrate(resolved.component, container);
|
|
2199
|
+
return;
|
|
1869
2200
|
}
|
|
1870
|
-
|
|
2201
|
+
replaceChildrenSafely(container);
|
|
2202
|
+
}).catch((err) => {
|
|
2203
|
+
if (typeof console !== "undefined") {
|
|
2204
|
+
console.error("[SibuJS routerSSR] failed to load hydrate:", err);
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
1871
2207
|
}
|
|
1872
2208
|
function createSSRRouter(routes) {
|
|
1873
2209
|
return {
|
|
@@ -1882,54 +2218,6 @@ function createSSRRouter(routes) {
|
|
|
1882
2218
|
}
|
|
1883
2219
|
};
|
|
1884
2220
|
}
|
|
1885
|
-
var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
|
|
1886
|
-
function isSafeAttrName(name) {
|
|
1887
|
-
return SAFE_ATTR_NAME.test(name);
|
|
1888
|
-
}
|
|
1889
|
-
function isEventHandlerAttr(name) {
|
|
1890
|
-
if (name.length < 3) return false;
|
|
1891
|
-
const lower = name.toLowerCase();
|
|
1892
|
-
return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
|
|
1893
|
-
}
|
|
1894
|
-
var URL_ATTRS = /* @__PURE__ */ new Set([
|
|
1895
|
-
"href",
|
|
1896
|
-
"src",
|
|
1897
|
-
"action",
|
|
1898
|
-
"formaction",
|
|
1899
|
-
"cite",
|
|
1900
|
-
"poster",
|
|
1901
|
-
"background",
|
|
1902
|
-
"srcset",
|
|
1903
|
-
"ping",
|
|
1904
|
-
"manifest",
|
|
1905
|
-
"data",
|
|
1906
|
-
"xlink:href"
|
|
1907
|
-
]);
|
|
1908
|
-
function sanitizeUrlLocal(url) {
|
|
1909
|
-
const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
|
|
1910
|
-
if (!trimmed) return "";
|
|
1911
|
-
const lower = trimmed.toLowerCase();
|
|
1912
|
-
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
|
|
1913
|
-
return "";
|
|
1914
|
-
}
|
|
1915
|
-
return trimmed;
|
|
1916
|
-
}
|
|
1917
|
-
function buildSafeAttrString(attrs) {
|
|
1918
|
-
const out = [];
|
|
1919
|
-
for (const rawKey of Object.keys(attrs)) {
|
|
1920
|
-
if (!Object.hasOwn(attrs, rawKey)) continue;
|
|
1921
|
-
if (!isSafeAttrName(rawKey)) continue;
|
|
1922
|
-
if (isEventHandlerAttr(rawKey)) continue;
|
|
1923
|
-
const lowerKey = rawKey.toLowerCase();
|
|
1924
|
-
let value = String(attrs[rawKey]);
|
|
1925
|
-
if (URL_ATTRS.has(lowerKey)) {
|
|
1926
|
-
value = sanitizeUrlLocal(value);
|
|
1927
|
-
if (!value) continue;
|
|
1928
|
-
}
|
|
1929
|
-
out.push(`${rawKey}="${escapeAttrLocal(value)}"`);
|
|
1930
|
-
}
|
|
1931
|
-
return out.join(" ");
|
|
1932
|
-
}
|
|
1933
2221
|
function escapeHtmlLocal(str) {
|
|
1934
2222
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1935
2223
|
}
|
|
@@ -1945,6 +2233,7 @@ export {
|
|
|
1945
2233
|
Suspense,
|
|
1946
2234
|
Trans,
|
|
1947
2235
|
VERSION,
|
|
2236
|
+
__getNavigationEpoch,
|
|
1948
2237
|
__removeRouterPagehideHandler,
|
|
1949
2238
|
addRoute,
|
|
1950
2239
|
afterEach,
|