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.cjs
CHANGED
|
@@ -48,19 +48,117 @@ var init_dev = __esm({
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
// src/core/errors.ts
|
|
52
|
+
function state() {
|
|
53
|
+
const g2 = globalThis;
|
|
54
|
+
const existing = g2[ERROR_STATE_KEY];
|
|
55
|
+
if (existing) return existing;
|
|
56
|
+
const created = { handler: null, handlerDepth: 0 };
|
|
57
|
+
g2[ERROR_STATE_KEY] = created;
|
|
58
|
+
return created;
|
|
59
|
+
}
|
|
60
|
+
function normalizeError(value) {
|
|
61
|
+
if (value instanceof Error) return value;
|
|
62
|
+
const message = typeof value === "string" ? value : `Non-Error value thrown: ${safeStringify(value)}`;
|
|
63
|
+
return new Error(message, { cause: value });
|
|
64
|
+
}
|
|
65
|
+
function safeStringify(value) {
|
|
66
|
+
if (typeof value === "symbol") return value.toString();
|
|
67
|
+
try {
|
|
68
|
+
return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
69
|
+
} catch {
|
|
70
|
+
return Object.prototype.toString.call(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function describe(context) {
|
|
74
|
+
return context.name ? `${context.phase} "${context.name}"` : context.phase;
|
|
75
|
+
}
|
|
76
|
+
function untrackedRun(fn) {
|
|
77
|
+
const api = globalThis[REACTIVE_REGISTRY_KEY];
|
|
78
|
+
return api?.untracked ? api.untracked(fn) : fn();
|
|
79
|
+
}
|
|
80
|
+
function claimedByBoundary(error, context) {
|
|
81
|
+
const raw = context.node;
|
|
82
|
+
if (!raw || typeof CustomEvent === "undefined") return false;
|
|
83
|
+
const target = resolveEventTarget(raw);
|
|
84
|
+
if (!target) return false;
|
|
85
|
+
try {
|
|
86
|
+
const event = new CustomEvent(PROPAGATE_EVENT, {
|
|
87
|
+
bubbles: true,
|
|
88
|
+
cancelable: true,
|
|
89
|
+
detail: { error, context }
|
|
90
|
+
});
|
|
91
|
+
return target.dispatchEvent(event) === false;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function resolveEventTarget(node) {
|
|
97
|
+
const ELEMENT_NODE = 1;
|
|
98
|
+
let current = node;
|
|
99
|
+
while (current) {
|
|
100
|
+
if (current.nodeType === ELEMENT_NODE && typeof current.dispatchEvent === "function") {
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
current = current.parentNode;
|
|
104
|
+
}
|
|
105
|
+
const self2 = node;
|
|
106
|
+
return typeof self2.dispatchEvent === "function" ? self2 : null;
|
|
107
|
+
}
|
|
108
|
+
function defaultReport(error, context) {
|
|
109
|
+
if (typeof console === "undefined") return;
|
|
110
|
+
console.error(`[SibuJS] Uncaught error during ${describe(context)}: ${error.message}`, error);
|
|
111
|
+
}
|
|
112
|
+
function reportError(error, context) {
|
|
113
|
+
const err = normalizeError(error);
|
|
114
|
+
untrackedRun(() => {
|
|
115
|
+
if (claimedByBoundary(err, context)) return;
|
|
116
|
+
const s2 = state();
|
|
117
|
+
const handler = s2.handler;
|
|
118
|
+
if (handler && s2.handlerDepth === 0) {
|
|
119
|
+
s2.handlerDepth++;
|
|
120
|
+
try {
|
|
121
|
+
handler(error instanceof Error ? error : err, context);
|
|
122
|
+
return;
|
|
123
|
+
} catch (handlerError) {
|
|
124
|
+
if (typeof console !== "undefined") {
|
|
125
|
+
const detail = handlerError instanceof Error ? handlerError.message : String(handlerError);
|
|
126
|
+
console.error(`[SibuJS] runtime error handler itself threw: ${detail}`, handlerError);
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
s2.handlerDepth--;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
defaultReport(err, context);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
var ERROR_STATE_KEY, REACTIVE_REGISTRY_KEY, PROPAGATE_EVENT;
|
|
136
|
+
var init_errors = __esm({
|
|
137
|
+
"src/core/errors.ts"() {
|
|
138
|
+
"use strict";
|
|
139
|
+
ERROR_STATE_KEY = /* @__PURE__ */ Symbol.for("sibujs.runtime-errors.v1");
|
|
140
|
+
REACTIVE_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
|
|
141
|
+
PROPAGATE_EVENT = "sibu:error-propagate";
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
51
145
|
// src/utils/sanitize.ts
|
|
52
146
|
function stripControlChars(value) {
|
|
53
147
|
return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
|
|
54
148
|
}
|
|
149
|
+
function canonicalAttrName(name) {
|
|
150
|
+
return name.replace(/[A-Z]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 32));
|
|
151
|
+
}
|
|
55
152
|
function isEventHandlerAttr(name) {
|
|
56
153
|
if (name.length < 3) return false;
|
|
57
154
|
const lower = name.toLowerCase();
|
|
58
155
|
return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
|
|
59
156
|
}
|
|
60
157
|
function sanitizeUrl(url) {
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
const
|
|
158
|
+
const probe = stripControlChars(url).trim();
|
|
159
|
+
if (!probe) return "";
|
|
160
|
+
const output2 = url.replace(OUTER_TRIM, "");
|
|
161
|
+
const lower = probe.toLowerCase();
|
|
64
162
|
let schemeEnd = -1;
|
|
65
163
|
for (let i2 = 0; i2 < lower.length; i2++) {
|
|
66
164
|
const ch = lower.charCodeAt(i2);
|
|
@@ -70,11 +168,11 @@ function sanitizeUrl(url) {
|
|
|
70
168
|
}
|
|
71
169
|
if (ch === 47 || ch === 63 || ch === 35) break;
|
|
72
170
|
}
|
|
73
|
-
if (schemeEnd === -1) return
|
|
171
|
+
if (schemeEnd === -1) return output2;
|
|
74
172
|
const scheme = lower.slice(0, schemeEnd + 1);
|
|
75
|
-
if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return
|
|
173
|
+
if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return output2;
|
|
76
174
|
if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
|
|
77
|
-
return
|
|
175
|
+
return output2;
|
|
78
176
|
}
|
|
79
177
|
function sanitizeSrcset(value) {
|
|
80
178
|
const parts = value.split(",");
|
|
@@ -84,43 +182,114 @@ function sanitizeSrcset(value) {
|
|
|
84
182
|
if (!part) continue;
|
|
85
183
|
const m = part.match(/^(\S+)(\s+.+)?$/);
|
|
86
184
|
if (!m) continue;
|
|
185
|
+
if (m[2] !== void 0 && !SRCSET_DESCRIPTOR.test(m[2])) continue;
|
|
87
186
|
const safe = sanitizeUrl(m[1]);
|
|
88
187
|
if (!safe) continue;
|
|
89
188
|
out.push(m[2] ? `${safe}${m[2]}` : safe);
|
|
90
189
|
}
|
|
91
190
|
return out.join(", ");
|
|
92
191
|
}
|
|
192
|
+
function isCssWhitespace(ch) {
|
|
193
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
|
|
194
|
+
}
|
|
195
|
+
function isHexDigit(ch) {
|
|
196
|
+
return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
|
|
197
|
+
}
|
|
198
|
+
function decodeCssEscapes(value) {
|
|
199
|
+
let out = "";
|
|
200
|
+
for (let i2 = 0; i2 < value.length; i2++) {
|
|
201
|
+
const ch = value[i2];
|
|
202
|
+
if (ch !== "\\") {
|
|
203
|
+
out += ch;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (i2 + 1 >= value.length) break;
|
|
207
|
+
const next = value[i2 + 1];
|
|
208
|
+
if (next === "\n" || next === "\f") {
|
|
209
|
+
i2 += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (next === "\r") {
|
|
213
|
+
i2 += value[i2 + 2] === "\n" ? 2 : 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (isHexDigit(next)) {
|
|
217
|
+
let hex = "";
|
|
218
|
+
let j = i2 + 1;
|
|
219
|
+
while (j < value.length && hex.length < 6 && isHexDigit(value[j])) {
|
|
220
|
+
hex += value[j];
|
|
221
|
+
j++;
|
|
222
|
+
}
|
|
223
|
+
if (j < value.length && isCssWhitespace(value[j])) {
|
|
224
|
+
j += value[j] === "\r" && value[j + 1] === "\n" ? 2 : 1;
|
|
225
|
+
}
|
|
226
|
+
const code2 = Number.parseInt(hex, 16);
|
|
227
|
+
const invalid = code2 === 0 || code2 > 1114111 || code2 >= 55296 && code2 <= 57343;
|
|
228
|
+
out += invalid ? "\uFFFD" : String.fromCodePoint(code2);
|
|
229
|
+
i2 = j - 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
out += next;
|
|
233
|
+
i2 += 1;
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
93
237
|
function sanitizeCSSValue(value) {
|
|
94
238
|
if (!CSS_DANGER_GATE.test(value)) return value;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
if (!Number.isFinite(code2) || code2 < 0 || code2 > 1114111) return "";
|
|
98
|
-
try {
|
|
99
|
-
return String.fromCodePoint(code2);
|
|
100
|
-
} catch {
|
|
101
|
-
return "";
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
const lower = decoded.toLowerCase().replace(/\s+/g, "");
|
|
239
|
+
const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
|
|
240
|
+
const lower = normalized.toLowerCase().replace(/\s+/g, "");
|
|
105
241
|
if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("vbscript:") || lower.includes("-moz-binding") || lower.includes("behavior:") || lower.includes("@import") || lower.includes("image-set(") || lower.includes("filter:progid")) {
|
|
106
242
|
return "";
|
|
107
243
|
}
|
|
108
244
|
return value;
|
|
109
245
|
}
|
|
246
|
+
function sanitizeStyleAttribute(cssText) {
|
|
247
|
+
const input2 = String(cssText);
|
|
248
|
+
if (input2.trim() === "") return "";
|
|
249
|
+
if (typeof document === "undefined") {
|
|
250
|
+
return sanitizeCSSValue(input2) === "" ? "" : input2;
|
|
251
|
+
}
|
|
252
|
+
const probe = document.createElement("div");
|
|
253
|
+
try {
|
|
254
|
+
probe.style.cssText = input2;
|
|
255
|
+
} catch {
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
const declarations = [];
|
|
259
|
+
for (let i2 = 0; i2 < probe.style.length; i2++) {
|
|
260
|
+
const property = probe.style[i2];
|
|
261
|
+
const value = probe.style.getPropertyValue(property);
|
|
262
|
+
if (sanitizeCSSValue(value) === "") continue;
|
|
263
|
+
if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
|
|
264
|
+
const priority = probe.style.getPropertyPriority(property);
|
|
265
|
+
declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
|
|
266
|
+
}
|
|
267
|
+
return declarations.join("; ");
|
|
268
|
+
}
|
|
269
|
+
function isHtmlContentAttribute(name) {
|
|
270
|
+
return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
|
|
271
|
+
}
|
|
110
272
|
function isUrlAttribute(attr) {
|
|
111
273
|
return URL_ATTRIBUTES.has(attr.toLowerCase());
|
|
112
274
|
}
|
|
275
|
+
function isPolicyAttribute(name) {
|
|
276
|
+
const lower = name.toLowerCase();
|
|
277
|
+
return lower === "style" || URL_ATTRIBUTES.has(lower);
|
|
278
|
+
}
|
|
113
279
|
function sanitizeAttributeString(attr, value) {
|
|
114
280
|
const lower = attr.toLowerCase();
|
|
115
281
|
if (lower === "srcset") return sanitizeSrcset(value);
|
|
116
282
|
if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
|
|
283
|
+
if (lower === "style") return sanitizeStyleAttribute(value);
|
|
117
284
|
return value;
|
|
118
285
|
}
|
|
119
|
-
var SAFE_URL_PROTOCOLS, CSS_DANGER_GATE, URL_ATTRIBUTES;
|
|
286
|
+
var SAFE_URL_PROTOCOLS, OUTER_TRIM, SRCSET_DESCRIPTOR, CSS_DANGER_GATE, URL_ATTRIBUTES, HTML_CONTENT_ATTRIBUTES;
|
|
120
287
|
var init_sanitize = __esm({
|
|
121
288
|
"src/utils/sanitize.ts"() {
|
|
122
289
|
"use strict";
|
|
123
290
|
SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
|
|
291
|
+
OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
|
|
292
|
+
SRCSET_DESCRIPTOR = /^\s+\d+(\.\d+)?[wx]$/;
|
|
124
293
|
CSS_DANGER_GATE = /[(:@\\]/;
|
|
125
294
|
URL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
126
295
|
"href",
|
|
@@ -134,8 +303,125 @@ var init_sanitize = __esm({
|
|
|
134
303
|
"background",
|
|
135
304
|
"srcset",
|
|
136
305
|
"ping",
|
|
137
|
-
"data"
|
|
306
|
+
"data",
|
|
307
|
+
// `<html manifest>` is a URL sink too. It was carried only by the SSR
|
|
308
|
+
// serializers' private copy of this set; folding that copy into this one would
|
|
309
|
+
// otherwise have silently dropped its coverage.
|
|
310
|
+
"manifest"
|
|
138
311
|
]);
|
|
312
|
+
HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// src/core/rendering/dispose.ts
|
|
317
|
+
var dispose_exports = {};
|
|
318
|
+
__export(dispose_exports, {
|
|
319
|
+
MAX_DRAIN_TEARDOWNS: () => MAX_DRAIN_TEARDOWNS,
|
|
320
|
+
checkLeaks: () => checkLeaks,
|
|
321
|
+
dispose: () => dispose,
|
|
322
|
+
registerDisposer: () => registerDisposer,
|
|
323
|
+
replaceChildrenSafely: () => replaceChildrenSafely,
|
|
324
|
+
reportDrainRunaway: () => reportDrainRunaway,
|
|
325
|
+
unregisterDisposer: () => unregisterDisposer
|
|
326
|
+
});
|
|
327
|
+
function reportDrainRunaway(label2, executed, remaining) {
|
|
328
|
+
if (typeof console === "undefined") return;
|
|
329
|
+
console.error(
|
|
330
|
+
`[SibuJS ${label2}] runaway cleanup: stopped after running ${executed} teardowns with ${remaining} still queued. Cleanup production did not stabilize before the teardown safety ceiling was reached \u2014 the remaining work was NOT run.`,
|
|
331
|
+
{ executed, remaining }
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
function registerDisposer(node, teardown) {
|
|
335
|
+
let disposers = elementDisposers.get(node);
|
|
336
|
+
if (!disposers) {
|
|
337
|
+
disposers = [];
|
|
338
|
+
elementDisposers.set(node, disposers);
|
|
339
|
+
}
|
|
340
|
+
disposers.push(teardown);
|
|
341
|
+
if (_isDev4) activeBindingCount++;
|
|
342
|
+
}
|
|
343
|
+
function unregisterDisposer(node, teardown) {
|
|
344
|
+
const disposers = elementDisposers.get(node);
|
|
345
|
+
if (!disposers) return;
|
|
346
|
+
const index = disposers.indexOf(teardown);
|
|
347
|
+
if (index === -1) return;
|
|
348
|
+
disposers.splice(index, 1);
|
|
349
|
+
if (_isDev4) activeBindingCount--;
|
|
350
|
+
if (disposers.length === 0) elementDisposers.delete(node);
|
|
351
|
+
}
|
|
352
|
+
function dispose(node) {
|
|
353
|
+
const stack = [node];
|
|
354
|
+
const order = [];
|
|
355
|
+
while (stack.length > 0) {
|
|
356
|
+
const current = stack.pop();
|
|
357
|
+
order.push(current);
|
|
358
|
+
const children = Array.from(current.childNodes);
|
|
359
|
+
for (let i2 = 0; i2 < children.length; i2++) {
|
|
360
|
+
stack.push(children[i2]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (let i2 = order.length - 1; i2 >= 0; i2--) {
|
|
364
|
+
const current = order[i2];
|
|
365
|
+
if (elementDisposers.has(current)) {
|
|
366
|
+
let executed = 0;
|
|
367
|
+
let runaway = false;
|
|
368
|
+
while (!runaway) {
|
|
369
|
+
const pending = elementDisposers.get(current);
|
|
370
|
+
if (!pending || pending.length === 0) break;
|
|
371
|
+
const snapshot = pending.slice();
|
|
372
|
+
elementDisposers.delete(current);
|
|
373
|
+
if (_isDev4) activeBindingCount -= snapshot.length;
|
|
374
|
+
for (let i3 = 0; i3 < snapshot.length; i3++) {
|
|
375
|
+
if (executed >= MAX_DRAIN_TEARDOWNS) {
|
|
376
|
+
const rest = snapshot.slice(i3);
|
|
377
|
+
const added = elementDisposers.get(current);
|
|
378
|
+
elementDisposers.set(current, added ? rest.concat(added) : rest);
|
|
379
|
+
if (_isDev4) activeBindingCount += rest.length;
|
|
380
|
+
reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
|
|
381
|
+
runaway = true;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
executed++;
|
|
385
|
+
try {
|
|
386
|
+
snapshot[i3]();
|
|
387
|
+
} catch (err) {
|
|
388
|
+
reportError(err, { phase: "cleanup", name: "disposer" });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function replaceChildrenSafely(parent, ...next) {
|
|
396
|
+
for (let i2 = 0; i2 < next.length; i2++) {
|
|
397
|
+
const node = next[i2];
|
|
398
|
+
node.parentNode?.removeChild(node);
|
|
399
|
+
}
|
|
400
|
+
const current = Array.from(parent.childNodes);
|
|
401
|
+
for (let i2 = 0; i2 < current.length; i2++) {
|
|
402
|
+
dispose(current[i2]);
|
|
403
|
+
}
|
|
404
|
+
parent.replaceChildren(...next);
|
|
405
|
+
}
|
|
406
|
+
function checkLeaks(warnThreshold = 0) {
|
|
407
|
+
if (!_isDev4) return 0;
|
|
408
|
+
if (warnThreshold > 0 && activeBindingCount > warnThreshold) {
|
|
409
|
+
devWarn(
|
|
410
|
+
`checkLeaks: ${activeBindingCount} active DOM bindings detected. Expected \u2264${warnThreshold}. This may indicate a component was removed from the DOM without calling dispose().`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return activeBindingCount;
|
|
414
|
+
}
|
|
415
|
+
var elementDisposers, MAX_DRAIN_TEARDOWNS, _isDev4, activeBindingCount;
|
|
416
|
+
var init_dispose = __esm({
|
|
417
|
+
"src/core/rendering/dispose.ts"() {
|
|
418
|
+
"use strict";
|
|
419
|
+
init_dev();
|
|
420
|
+
init_errors();
|
|
421
|
+
elementDisposers = /* @__PURE__ */ new WeakMap();
|
|
422
|
+
MAX_DRAIN_TEARDOWNS = 1e4;
|
|
423
|
+
_isDev4 = isDev();
|
|
424
|
+
activeBindingCount = 0;
|
|
139
425
|
}
|
|
140
426
|
});
|
|
141
427
|
|
|
@@ -148,16 +434,19 @@ function detectSSRShared() {
|
|
|
148
434
|
const getBuiltin = process.getBuiltinModule;
|
|
149
435
|
if (typeof getBuiltin === "function") {
|
|
150
436
|
mod = getBuiltin("node:async_hooks");
|
|
151
|
-
} else {
|
|
152
|
-
|
|
153
|
-
if (req) mod = req("node:async_hooks");
|
|
437
|
+
} else if (typeof require === "function") {
|
|
438
|
+
mod = require("async_hooks");
|
|
154
439
|
}
|
|
155
440
|
if (mod) detected = new mod.AsyncLocalStorage();
|
|
156
441
|
}
|
|
157
442
|
} catch {
|
|
158
443
|
detected = null;
|
|
159
444
|
}
|
|
160
|
-
return {
|
|
445
|
+
return {
|
|
446
|
+
als: detected,
|
|
447
|
+
fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
|
|
448
|
+
fallbackDepth: 0
|
|
449
|
+
};
|
|
161
450
|
}
|
|
162
451
|
function getSSRStore() {
|
|
163
452
|
if (als) {
|
|
@@ -166,6 +455,10 @@ function getSSRStore() {
|
|
|
166
455
|
}
|
|
167
456
|
return fallbackStore;
|
|
168
457
|
}
|
|
458
|
+
function getRequestStore() {
|
|
459
|
+
if (als) return als.getStore() ?? null;
|
|
460
|
+
return _shared.fallbackDepth > 0 ? fallbackStore : null;
|
|
461
|
+
}
|
|
169
462
|
function isSSR() {
|
|
170
463
|
return getSSRStore().ssr;
|
|
171
464
|
}
|
|
@@ -177,6 +470,179 @@ var init_ssr_context = __esm({
|
|
|
177
470
|
_shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
|
|
178
471
|
als = _shared.als;
|
|
179
472
|
fallbackStore = _shared.fallbackStore;
|
|
473
|
+
_shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// src/utils/metaRefresh.ts
|
|
478
|
+
function isAsciiWhitespace(ch) {
|
|
479
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
|
|
480
|
+
}
|
|
481
|
+
function trimAsciiWhitespace(value) {
|
|
482
|
+
let start = 0;
|
|
483
|
+
let end = value.length;
|
|
484
|
+
while (start < end && isAsciiWhitespace(value[start])) start++;
|
|
485
|
+
while (end > start && isAsciiWhitespace(value[end - 1])) end--;
|
|
486
|
+
return value.slice(start, end);
|
|
487
|
+
}
|
|
488
|
+
function parseMetaRefreshContent(content) {
|
|
489
|
+
const raw = trimAsciiWhitespace(stripControlChars(content));
|
|
490
|
+
if (raw === "") return forbidden("empty content");
|
|
491
|
+
let i2 = 0;
|
|
492
|
+
while (i2 < raw.length && raw[i2] >= "0" && raw[i2] <= "9") i2++;
|
|
493
|
+
if (i2 === 0) return forbidden("missing or non-numeric delay");
|
|
494
|
+
const delay = Number.parseInt(raw.slice(0, i2), 10);
|
|
495
|
+
if (!Number.isFinite(delay)) return forbidden("unparseable delay");
|
|
496
|
+
while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
|
|
497
|
+
if (i2 === raw.length) return { kind: "delay-only", delay };
|
|
498
|
+
if (raw[i2] !== ";" && raw[i2] !== ",") return forbidden("unexpected text after delay");
|
|
499
|
+
i2++;
|
|
500
|
+
while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
|
|
501
|
+
const keyStart = i2;
|
|
502
|
+
while (i2 < raw.length && !isAsciiWhitespace(raw[i2]) && raw[i2] !== "=") i2++;
|
|
503
|
+
const key = raw.slice(keyStart, i2).toLowerCase();
|
|
504
|
+
if (key !== "url") return forbidden(`unsupported refresh key ${JSON.stringify(key)}`);
|
|
505
|
+
while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
|
|
506
|
+
if (raw[i2] !== "=") return forbidden("missing '=' after url");
|
|
507
|
+
i2++;
|
|
508
|
+
while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
|
|
509
|
+
let destination;
|
|
510
|
+
const quote = raw[i2];
|
|
511
|
+
if (quote === '"' || quote === "'") {
|
|
512
|
+
const close = raw.indexOf(quote, i2 + 1);
|
|
513
|
+
if (close === -1) return forbidden("unterminated quoted destination");
|
|
514
|
+
destination = raw.slice(i2 + 1, close);
|
|
515
|
+
const rest = trimAsciiWhitespace(raw.slice(close + 1));
|
|
516
|
+
if (rest !== "") return forbidden("trailing text after quoted destination");
|
|
517
|
+
} else {
|
|
518
|
+
destination = trimAsciiWhitespace(raw.slice(i2));
|
|
519
|
+
if (/[;,]/.test(destination)) return forbidden("multiple refresh assignments");
|
|
520
|
+
}
|
|
521
|
+
if (destination === "") return forbidden("empty destination");
|
|
522
|
+
const sanitizedDestination = sanitizeUrl(destination);
|
|
523
|
+
if (sanitizedDestination === "") return forbidden("destination uses a disallowed protocol");
|
|
524
|
+
return { kind: "allowed", delay, destination, sanitizedDestination };
|
|
525
|
+
}
|
|
526
|
+
function resolveMetaRefreshPolicy(attributes) {
|
|
527
|
+
let httpEquiv;
|
|
528
|
+
let content;
|
|
529
|
+
let duplicateName;
|
|
530
|
+
const seen = /* @__PURE__ */ new Set();
|
|
531
|
+
for (const [key, value] of attributes) {
|
|
532
|
+
const canonical = canonicalAttrName(key);
|
|
533
|
+
if (seen.has(canonical)) duplicateName = canonical;
|
|
534
|
+
seen.add(canonical);
|
|
535
|
+
if (canonical === "http-equiv") httpEquiv = value;
|
|
536
|
+
else if (canonical === "content") content = value;
|
|
537
|
+
}
|
|
538
|
+
if (duplicateName !== void 0) {
|
|
539
|
+
return forbidden(`duplicate case-insensitive attribute ${JSON.stringify(duplicateName)}`);
|
|
540
|
+
}
|
|
541
|
+
if (typeof httpEquiv !== "string") return { kind: "not-refresh" };
|
|
542
|
+
if (trimAsciiWhitespace(stripControlChars(httpEquiv)).toLowerCase() !== "refresh") {
|
|
543
|
+
return { kind: "not-refresh" };
|
|
544
|
+
}
|
|
545
|
+
if (typeof content !== "string") return { kind: "not-refresh" };
|
|
546
|
+
return parseMetaRefreshContent(content);
|
|
547
|
+
}
|
|
548
|
+
function normalizeMetaAttributes(props) {
|
|
549
|
+
const out = /* @__PURE__ */ new Map();
|
|
550
|
+
const seen = /* @__PURE__ */ new Set();
|
|
551
|
+
for (const key of Object.keys(props)) {
|
|
552
|
+
const canonical = canonicalAttrName(key);
|
|
553
|
+
if (seen.has(canonical)) return null;
|
|
554
|
+
seen.add(canonical);
|
|
555
|
+
out.set(key, props[key]);
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
function isForbiddenMetaEntry(props) {
|
|
560
|
+
const normalized = normalizeMetaAttributes(props);
|
|
561
|
+
if (normalized === null) return true;
|
|
562
|
+
return resolveMetaRefreshPolicy(normalized).kind === "forbidden";
|
|
563
|
+
}
|
|
564
|
+
var forbidden;
|
|
565
|
+
var init_metaRefresh = __esm({
|
|
566
|
+
"src/utils/metaRefresh.ts"() {
|
|
567
|
+
"use strict";
|
|
568
|
+
init_sanitize();
|
|
569
|
+
forbidden = (reason) => ({ kind: "forbidden", reason });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// src/utils/headEntry.ts
|
|
574
|
+
function isEmittableHeadAttrName(name) {
|
|
575
|
+
if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
|
|
576
|
+
const canonical = canonicalAttrName(name);
|
|
577
|
+
if (isEventHandlerAttr(canonical)) return false;
|
|
578
|
+
if (isHtmlContentAttribute(canonical)) return false;
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
function sanitizeHeadAttrValue(canonical, value) {
|
|
582
|
+
if (!isPolicyAttribute(canonical)) return value;
|
|
583
|
+
const safe = sanitizeAttributeString(canonical, value);
|
|
584
|
+
return safe === "" ? null : safe;
|
|
585
|
+
}
|
|
586
|
+
function findDuplicateAttributeName(names) {
|
|
587
|
+
const seen = /* @__PURE__ */ new Set();
|
|
588
|
+
for (const name of names) {
|
|
589
|
+
const canonical = canonicalAttrName(name);
|
|
590
|
+
if (seen.has(canonical)) return canonical;
|
|
591
|
+
seen.add(canonical);
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function planEntry(props, resolver, applyRefreshPolicy) {
|
|
596
|
+
const rawNames = Object.keys(props);
|
|
597
|
+
const duplicate = findDuplicateAttributeName(rawNames);
|
|
598
|
+
if (duplicate !== null) {
|
|
599
|
+
return { kind: "drop", reason: `duplicate case-insensitive attribute ${JSON.stringify(duplicate)}` };
|
|
600
|
+
}
|
|
601
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
602
|
+
let reactive = false;
|
|
603
|
+
for (const name of rawNames) {
|
|
604
|
+
if (!Object.hasOwn(props, name)) continue;
|
|
605
|
+
if (!isEmittableHeadAttrName(name)) continue;
|
|
606
|
+
const authored = props[name];
|
|
607
|
+
if (resolver.isReactiveValue?.(authored)) reactive = true;
|
|
608
|
+
const canonical = canonicalAttrName(name);
|
|
609
|
+
const sanitized = sanitizeHeadAttrValue(canonical, resolver.resolveValue(name, authored));
|
|
610
|
+
if (sanitized === null) continue;
|
|
611
|
+
attributes.set(canonical, sanitized);
|
|
612
|
+
}
|
|
613
|
+
if (attributes.size === 0) return { kind: "drop", reason: "no effective attributes remain" };
|
|
614
|
+
const refresh = applyRefreshPolicy ? resolveMetaRefreshPolicy(attributes) : { kind: "not-refresh" };
|
|
615
|
+
if (refresh.kind === "forbidden") return { kind: "drop", reason: refresh.reason };
|
|
616
|
+
if (reactive && (refresh.kind === "allowed" || refresh.kind === "delay-only")) {
|
|
617
|
+
return { kind: "drop", reason: "a reactive meta entry may not publish a native refresh directive" };
|
|
618
|
+
}
|
|
619
|
+
return { kind: "publish", attributes, refresh };
|
|
620
|
+
}
|
|
621
|
+
function planMetaEntry(props, resolver) {
|
|
622
|
+
return planEntry(props, resolver, true);
|
|
623
|
+
}
|
|
624
|
+
function planHeadElementEntry(props, resolver) {
|
|
625
|
+
return planEntry(props, resolver, false);
|
|
626
|
+
}
|
|
627
|
+
function escapeHeadAttrValue(value) {
|
|
628
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
629
|
+
}
|
|
630
|
+
function serializeHeadEntry(tag, attrs) {
|
|
631
|
+
const plan = tag === "meta" ? planMetaEntry(attrs, STATIC_VALUE_RESOLVER) : planHeadElementEntry(attrs, STATIC_VALUE_RESOLVER);
|
|
632
|
+
if (plan.kind === "drop") return "";
|
|
633
|
+
const pairs = Array.from(plan.attributes, ([name, value]) => `${name}="${escapeHeadAttrValue(value)}"`).join(" ");
|
|
634
|
+
return `<${tag} ${pairs} />`;
|
|
635
|
+
}
|
|
636
|
+
var SAFE_HEAD_ATTR_NAME, STATIC_VALUE_RESOLVER;
|
|
637
|
+
var init_headEntry = __esm({
|
|
638
|
+
"src/utils/headEntry.ts"() {
|
|
639
|
+
"use strict";
|
|
640
|
+
init_metaRefresh();
|
|
641
|
+
init_sanitize();
|
|
642
|
+
SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
|
|
643
|
+
STATIC_VALUE_RESOLVER = {
|
|
644
|
+
resolveValue: (_name, value) => String(value)
|
|
645
|
+
};
|
|
180
646
|
}
|
|
181
647
|
});
|
|
182
648
|
|
|
@@ -202,14 +668,20 @@ __export(ssr_exports, {
|
|
|
202
668
|
suspenseSwapScript: () => suspenseSwapScript,
|
|
203
669
|
trustHTML: () => trustHTML
|
|
204
670
|
});
|
|
205
|
-
function
|
|
206
|
-
return
|
|
671
|
+
function isPolicyAttr(lowerName) {
|
|
672
|
+
return isPolicyAttribute(lowerName);
|
|
673
|
+
}
|
|
674
|
+
function isForbiddenSsrAttr(lowerName) {
|
|
675
|
+
return isHtmlContentAttribute(lowerName);
|
|
676
|
+
}
|
|
677
|
+
function sanitizeSsrAttributeValue(lowerName, value) {
|
|
678
|
+
return sanitizeAttributeString(lowerName, value);
|
|
207
679
|
}
|
|
208
680
|
function isSafeAttrName(name) {
|
|
209
681
|
return SAFE_ATTR_NAME.test(name);
|
|
210
682
|
}
|
|
211
683
|
function ssrErrorComment(err) {
|
|
212
|
-
if (
|
|
684
|
+
if (_isDev8) {
|
|
213
685
|
const msg = escapeHtml(err instanceof Error ? err.message : String(err));
|
|
214
686
|
return `<!--SSR error: ${safeCommentText(msg)}-->`;
|
|
215
687
|
}
|
|
@@ -239,20 +711,21 @@ function renderToString(element) {
|
|
|
239
711
|
}
|
|
240
712
|
const tag = element.tagName.toLowerCase();
|
|
241
713
|
if (tag === "script" || tag === "style") {
|
|
242
|
-
return
|
|
714
|
+
return _isDev8 ? `<!--ssr:${tag}-stripped-->` : "";
|
|
243
715
|
}
|
|
244
716
|
if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
|
|
245
|
-
return
|
|
717
|
+
return _isDev8 ? "<!--ssr:invalid-tag-->" : "";
|
|
246
718
|
}
|
|
247
719
|
let html2 = `<${tag}`;
|
|
248
720
|
for (const attr of Array.from(element.attributes)) {
|
|
249
721
|
const rawName = attr.name;
|
|
250
722
|
if (!isSafeAttrName(rawName)) continue;
|
|
251
723
|
if (isEventHandlerAttr(rawName)) continue;
|
|
724
|
+
if (isForbiddenSsrAttr(rawName)) continue;
|
|
252
725
|
const lowerName = rawName.toLowerCase();
|
|
253
726
|
let value = attr.value;
|
|
254
|
-
if (
|
|
255
|
-
value =
|
|
727
|
+
if (isPolicyAttr(lowerName)) {
|
|
728
|
+
value = sanitizeSsrAttributeValue(lowerName, value);
|
|
256
729
|
if (!value) continue;
|
|
257
730
|
}
|
|
258
731
|
html2 += ` ${rawName}="${escapeAttr(value)}"`;
|
|
@@ -283,7 +756,7 @@ function hydrate(component, container, options = {}) {
|
|
|
283
756
|
const first = mismatches[0];
|
|
284
757
|
if (options.onMismatch) {
|
|
285
758
|
options.onMismatch(first);
|
|
286
|
-
} else if (
|
|
759
|
+
} else if (_isDev8) {
|
|
287
760
|
console.warn(
|
|
288
761
|
`[SibuJS hydration] ${first.message}
|
|
289
762
|
at ${first.path}
|
|
@@ -293,9 +766,20 @@ function hydrate(component, container, options = {}) {
|
|
|
293
766
|
}
|
|
294
767
|
}
|
|
295
768
|
}
|
|
296
|
-
container
|
|
769
|
+
replaceChildrenSafely(container, clientTree);
|
|
297
770
|
container.setAttribute("data-sibu-hydrated", "true");
|
|
298
771
|
}
|
|
772
|
+
function directTextOf(node) {
|
|
773
|
+
let out = "";
|
|
774
|
+
for (const child of Array.from(node.childNodes)) {
|
|
775
|
+
if (child.nodeType === 3) out += child.nodeValue ?? "";
|
|
776
|
+
}
|
|
777
|
+
return out.replace(/\s+/g, " ").trim();
|
|
778
|
+
}
|
|
779
|
+
function disposeAndReplace(outgoing, next) {
|
|
780
|
+
dispose(outgoing);
|
|
781
|
+
outgoing.replaceWith(next);
|
|
782
|
+
}
|
|
299
783
|
function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
|
|
300
784
|
if (out.length >= max) return;
|
|
301
785
|
const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
|
|
@@ -371,6 +855,18 @@ function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
|
|
|
371
855
|
});
|
|
372
856
|
}
|
|
373
857
|
}
|
|
858
|
+
const serverText = directTextOf(serverNode);
|
|
859
|
+
const clientText = directTextOf(clientNode);
|
|
860
|
+
if (serverText !== clientText) {
|
|
861
|
+
out.push({
|
|
862
|
+
kind: "text",
|
|
863
|
+
path: nodePath,
|
|
864
|
+
serverValue: serverText,
|
|
865
|
+
clientValue: clientText,
|
|
866
|
+
message: "Text content differs between server and client."
|
|
867
|
+
});
|
|
868
|
+
if (out.length >= max) return;
|
|
869
|
+
}
|
|
374
870
|
const serverChildren = Array.from(serverNode.children);
|
|
375
871
|
const clientChildren = Array.from(clientNode.children);
|
|
376
872
|
const max2 = Math.max(serverChildren.length, clientChildren.length);
|
|
@@ -390,10 +886,11 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
|
|
|
390
886
|
if (!Object.hasOwn(attrs, rawKey)) continue;
|
|
391
887
|
if (!isSafeAttrName(rawKey)) continue;
|
|
392
888
|
if (!allowEventHandlers && isEventHandlerAttr(rawKey)) continue;
|
|
889
|
+
if (isForbiddenSsrAttr(rawKey)) continue;
|
|
393
890
|
const lowerKey = rawKey.toLowerCase();
|
|
394
891
|
let value = String(attrs[rawKey]);
|
|
395
|
-
if (
|
|
396
|
-
value =
|
|
892
|
+
if (isPolicyAttr(lowerKey)) {
|
|
893
|
+
value = sanitizeSsrAttributeValue(lowerKey, value);
|
|
397
894
|
if (!value) continue;
|
|
398
895
|
}
|
|
399
896
|
out.push(`${rawKey}="${escapeAttr(value)}"`);
|
|
@@ -401,18 +898,7 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
|
|
|
401
898
|
return out.join(" ");
|
|
402
899
|
}
|
|
403
900
|
function isDangerousMetaRefresh(metaProps) {
|
|
404
|
-
|
|
405
|
-
let content;
|
|
406
|
-
for (const k in metaProps) {
|
|
407
|
-
const lk = k.toLowerCase();
|
|
408
|
-
if (lk === "http-equiv") httpEquiv = metaProps[k];
|
|
409
|
-
else if (lk === "content") content = metaProps[k];
|
|
410
|
-
}
|
|
411
|
-
if (typeof httpEquiv !== "string") return false;
|
|
412
|
-
if (httpEquiv.toLowerCase() !== "refresh") return false;
|
|
413
|
-
if (typeof content !== "string") return false;
|
|
414
|
-
const normalized = stripControlChars(content).toLowerCase();
|
|
415
|
-
return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
|
|
901
|
+
return isForbiddenMetaEntry(metaProps);
|
|
416
902
|
}
|
|
417
903
|
function renderToDocument(component, options = {}) {
|
|
418
904
|
let content;
|
|
@@ -421,15 +907,8 @@ function renderToDocument(component, options = {}) {
|
|
|
421
907
|
} catch (err) {
|
|
422
908
|
content = ssrErrorComment(err);
|
|
423
909
|
}
|
|
424
|
-
const metaTags = (options.meta || []).map((attrs) =>
|
|
425
|
-
|
|
426
|
-
const pairs = buildAttrString(attrs);
|
|
427
|
-
return pairs ? `<meta ${pairs} />` : "";
|
|
428
|
-
}).filter(Boolean).join("\n ");
|
|
429
|
-
const linkTags = (options.links || []).map((attrs) => {
|
|
430
|
-
const pairs = buildAttrString(attrs);
|
|
431
|
-
return pairs ? `<link ${pairs} />` : "";
|
|
432
|
-
}).filter(Boolean).join("\n ");
|
|
910
|
+
const metaTags = (options.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
|
|
911
|
+
const linkTags = (options.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
|
|
433
912
|
const scriptTags = (options.scripts || []).map((src) => {
|
|
434
913
|
const safe = sanitizeUrl(String(src));
|
|
435
914
|
if (!safe) return "";
|
|
@@ -478,11 +957,11 @@ async function* renderToStream(element) {
|
|
|
478
957
|
}
|
|
479
958
|
const tag = element.tagName.toLowerCase();
|
|
480
959
|
if (tag === "script" || tag === "style") {
|
|
481
|
-
if (
|
|
960
|
+
if (_isDev8) yield `<!--ssr:${tag}-stripped-->`;
|
|
482
961
|
return;
|
|
483
962
|
}
|
|
484
963
|
if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
|
|
485
|
-
if (
|
|
964
|
+
if (_isDev8) yield "<!--ssr:invalid-tag-->";
|
|
486
965
|
return;
|
|
487
966
|
}
|
|
488
967
|
let openTag = `<${tag}`;
|
|
@@ -490,14 +969,18 @@ async function* renderToStream(element) {
|
|
|
490
969
|
const rawName = attr.name;
|
|
491
970
|
if (!isSafeAttrName(rawName)) continue;
|
|
492
971
|
if (isEventHandlerAttr(rawName)) continue;
|
|
972
|
+
if (isForbiddenSsrAttr(rawName)) continue;
|
|
493
973
|
const lowerName = rawName.toLowerCase();
|
|
494
974
|
let value = attr.value;
|
|
495
|
-
if (
|
|
496
|
-
value =
|
|
975
|
+
if (isPolicyAttr(lowerName)) {
|
|
976
|
+
value = sanitizeSsrAttributeValue(lowerName, value);
|
|
497
977
|
if (!value) continue;
|
|
498
978
|
}
|
|
499
979
|
openTag += ` ${rawName}="${escapeAttr(value)}"`;
|
|
500
980
|
}
|
|
981
|
+
if (element.dataset && !element.dataset.sibuHydrate) {
|
|
982
|
+
openTag += ` data-sibu-ssr="true"`;
|
|
983
|
+
}
|
|
501
984
|
if (VOID_ELEMENTS.has(tag)) {
|
|
502
985
|
yield `${openTag} />`;
|
|
503
986
|
return;
|
|
@@ -543,9 +1026,13 @@ function island(id, component) {
|
|
|
543
1026
|
el.setAttribute("data-sibu-island", id);
|
|
544
1027
|
return el;
|
|
545
1028
|
}
|
|
1029
|
+
function isAlreadyHydrated(element) {
|
|
1030
|
+
return element.hasAttribute("data-sibu-hydrated");
|
|
1031
|
+
}
|
|
546
1032
|
function hydrateIslands(container, islands) {
|
|
547
1033
|
const markers = container.querySelectorAll("[data-sibu-island]");
|
|
548
1034
|
for (const marker2 of Array.from(markers)) {
|
|
1035
|
+
if (isAlreadyHydrated(marker2)) continue;
|
|
549
1036
|
const id = marker2.getAttribute("data-sibu-island") ?? "";
|
|
550
1037
|
if (!Object.hasOwn(islands, id)) continue;
|
|
551
1038
|
const factory = islands[id];
|
|
@@ -553,7 +1040,7 @@ function hydrateIslands(container, islands) {
|
|
|
553
1040
|
const clientTree = factory();
|
|
554
1041
|
clientTree.setAttribute("data-sibu-island", id);
|
|
555
1042
|
clientTree.setAttribute("data-sibu-hydrated", "true");
|
|
556
|
-
marker2
|
|
1043
|
+
disposeAndReplace(marker2, clientTree);
|
|
557
1044
|
}
|
|
558
1045
|
container.setAttribute("data-sibu-hydrated", "partial");
|
|
559
1046
|
}
|
|
@@ -561,6 +1048,7 @@ function hydrateProgressively(container, islands, options) {
|
|
|
561
1048
|
const markers = container.querySelectorAll("[data-sibu-island]");
|
|
562
1049
|
const cleanups = [];
|
|
563
1050
|
for (const marker2 of Array.from(markers)) {
|
|
1051
|
+
if (isAlreadyHydrated(marker2)) continue;
|
|
564
1052
|
const id = marker2.getAttribute("data-sibu-island") ?? "";
|
|
565
1053
|
if (!Object.hasOwn(islands, id)) continue;
|
|
566
1054
|
const factory = islands[id];
|
|
@@ -570,10 +1058,11 @@ function hydrateProgressively(container, islands, options) {
|
|
|
570
1058
|
for (const entry of entries) {
|
|
571
1059
|
if (entry.isIntersecting) {
|
|
572
1060
|
observer.disconnect();
|
|
1061
|
+
if (isAlreadyHydrated(marker2) || marker2.parentNode === null) break;
|
|
573
1062
|
const clientTree = factory();
|
|
574
1063
|
clientTree.setAttribute("data-sibu-island", id);
|
|
575
1064
|
clientTree.setAttribute("data-sibu-hydrated", "true");
|
|
576
|
-
marker2
|
|
1065
|
+
disposeAndReplace(marker2, clientTree);
|
|
577
1066
|
break;
|
|
578
1067
|
}
|
|
579
1068
|
}
|
|
@@ -614,7 +1103,7 @@ function ssrSuspense(props) {
|
|
|
614
1103
|
},
|
|
615
1104
|
(err) => {
|
|
616
1105
|
if (timer) clearTimeout(timer);
|
|
617
|
-
if (
|
|
1106
|
+
if (_isDev8) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
|
|
618
1107
|
return { id, html: fallbackHtml };
|
|
619
1108
|
}
|
|
620
1109
|
);
|
|
@@ -628,7 +1117,7 @@ function suspenseSwapScript(id, nonce) {
|
|
|
628
1117
|
);
|
|
629
1118
|
}
|
|
630
1119
|
const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
631
|
-
return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
|
|
1120
|
+
return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(f.firstChild)f.removeChild(f.firstChild);while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
|
|
632
1121
|
}
|
|
633
1122
|
async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
|
|
634
1123
|
yield* renderToStream(element);
|
|
@@ -644,8 +1133,8 @@ async function* renderToSuspenseStream(element, pendingBoundaries = [], options)
|
|
|
644
1133
|
function escapeScriptJson(json) {
|
|
645
1134
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
646
1135
|
}
|
|
647
|
-
function serializeState(
|
|
648
|
-
const rawJson = JSON.stringify(
|
|
1136
|
+
function serializeState(state2, nonce, options) {
|
|
1137
|
+
const rawJson = JSON.stringify(state2);
|
|
649
1138
|
const maxBytes = options?.maxBytes ?? DEFAULT_MAX_SSR_BYTES;
|
|
650
1139
|
const byteLen = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(rawJson).byteLength : Buffer.byteLength(rawJson, "utf8");
|
|
651
1140
|
if (byteLen > maxBytes) {
|
|
@@ -657,7 +1146,7 @@ function serializeState(state, nonce, options) {
|
|
|
657
1146
|
}
|
|
658
1147
|
function deserializeState(validate) {
|
|
659
1148
|
if (typeof window === "undefined") return void 0;
|
|
660
|
-
if (
|
|
1149
|
+
if (_isDev8 && !validate) {
|
|
661
1150
|
console.warn(
|
|
662
1151
|
"[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
|
|
663
1152
|
);
|
|
@@ -674,29 +1163,18 @@ function escapeHtml(str) {
|
|
|
674
1163
|
function escapeAttr(str) {
|
|
675
1164
|
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
676
1165
|
}
|
|
677
|
-
var
|
|
1166
|
+
var _isDev8, SAFE_ATTR_NAME, VOID_ELEMENTS, SAFE_ID, SAFE_SUSPENSE_ID, SSR_DATA_ATTR, DEFAULT_MAX_SSR_BYTES;
|
|
678
1167
|
var init_ssr = __esm({
|
|
679
1168
|
"src/platform/ssr.ts"() {
|
|
680
1169
|
"use strict";
|
|
681
1170
|
init_dev();
|
|
1171
|
+
init_dispose();
|
|
682
1172
|
init_ssr_context();
|
|
1173
|
+
init_headEntry();
|
|
1174
|
+
init_metaRefresh();
|
|
683
1175
|
init_sanitize();
|
|
684
|
-
|
|
1176
|
+
_isDev8 = isDev();
|
|
685
1177
|
SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
|
|
686
|
-
URL_ATTRS = /* @__PURE__ */ new Set([
|
|
687
|
-
"href",
|
|
688
|
-
"src",
|
|
689
|
-
"action",
|
|
690
|
-
"formaction",
|
|
691
|
-
"cite",
|
|
692
|
-
"poster",
|
|
693
|
-
"background",
|
|
694
|
-
"srcset",
|
|
695
|
-
"ping",
|
|
696
|
-
"manifest",
|
|
697
|
-
"data",
|
|
698
|
-
"xlink:href"
|
|
699
|
-
]);
|
|
700
1178
|
VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
701
1179
|
"area",
|
|
702
1180
|
"base",
|
|
@@ -731,6 +1209,7 @@ __export(plugins_exports, {
|
|
|
731
1209
|
Suspense: () => Suspense,
|
|
732
1210
|
Trans: () => Trans,
|
|
733
1211
|
VERSION: () => VERSION,
|
|
1212
|
+
__getNavigationEpoch: () => __getNavigationEpoch,
|
|
734
1213
|
__removeRouterPagehideHandler: () => __removeRouterPagehideHandler,
|
|
735
1214
|
addRoute: () => addRoute,
|
|
736
1215
|
afterEach: () => afterEach,
|
|
@@ -805,15 +1284,92 @@ module.exports = __toCommonJS(plugins_exports);
|
|
|
805
1284
|
init_dev();
|
|
806
1285
|
|
|
807
1286
|
// src/reactivity/bindAttribute.ts
|
|
1287
|
+
init_errors();
|
|
1288
|
+
init_sanitize();
|
|
1289
|
+
|
|
1290
|
+
// src/utils/setSafeAttribute.ts
|
|
808
1291
|
init_dev();
|
|
809
1292
|
init_sanitize();
|
|
1293
|
+
var _isDev2 = isDev();
|
|
1294
|
+
var HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
1295
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
1296
|
+
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
1297
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
1298
|
+
var BOOLEAN_IDL_ATTRS = /* @__PURE__ */ new Set(["checked", "disabled", "selected"]);
|
|
1299
|
+
function isHtmlElement(el) {
|
|
1300
|
+
return el.namespaceURI === null || el.namespaceURI === HTML_NS;
|
|
1301
|
+
}
|
|
1302
|
+
function setProp(el, key, val) {
|
|
1303
|
+
el[key] = val;
|
|
1304
|
+
}
|
|
1305
|
+
function namespaceFor(el, name) {
|
|
1306
|
+
if (el.namespaceURI !== SVG_NS) return null;
|
|
1307
|
+
const lower = name.toLowerCase();
|
|
1308
|
+
if (lower.startsWith("xlink:")) return XLINK_NS;
|
|
1309
|
+
if (lower.startsWith("xml:")) return XML_NS;
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
function setSafeAttribute(el, name, value, options = {}) {
|
|
1313
|
+
const ns = namespaceFor(el, name);
|
|
1314
|
+
const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
|
|
1315
|
+
if (isEventHandlerAttr(name)) {
|
|
1316
|
+
if (_isDev2) {
|
|
1317
|
+
devWarn(
|
|
1318
|
+
`${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
if (ns) el.removeAttributeNS(ns, localName);
|
|
1322
|
+
else el.removeAttribute(name);
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
if (value == null) {
|
|
1326
|
+
if (ns) el.removeAttributeNS(ns, localName);
|
|
1327
|
+
else el.removeAttribute(name);
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
if (isHtmlContentAttribute(name)) {
|
|
1331
|
+
if (_isDev2) {
|
|
1332
|
+
devWarn(
|
|
1333
|
+
`${options.label ?? "setSafeAttribute"}: refusing to set "${name}". The browser parses this attribute as a nested HTML document, so a generic string value cannot be made safe \u2014 attribute escaping is undone before the parse. Build the frame's content another way.`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (ns) el.removeAttributeNS(ns, localName);
|
|
1337
|
+
else el.removeAttribute(name);
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
const html2 = isHtmlElement(el);
|
|
1341
|
+
const idlName = html2 ? name.toLowerCase() : name;
|
|
1342
|
+
if (typeof value === "boolean") {
|
|
1343
|
+
if (html2 && BOOLEAN_IDL_ATTRS.has(idlName) && idlName in el) {
|
|
1344
|
+
setProp(el, idlName, value);
|
|
1345
|
+
} else if (value) {
|
|
1346
|
+
if (ns) el.setAttributeNS(ns, name, "");
|
|
1347
|
+
else el.setAttribute(name, "");
|
|
1348
|
+
} else if (ns) {
|
|
1349
|
+
el.removeAttributeNS(ns, localName);
|
|
1350
|
+
} else {
|
|
1351
|
+
el.removeAttribute(name);
|
|
1352
|
+
}
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
const str = String(value);
|
|
1356
|
+
if (options.syncValueProperty !== false && html2 && (idlName === "value" || idlName === "checked") && idlName in el) {
|
|
1357
|
+
setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
const safe = sanitizeAttributeString(name, str);
|
|
1361
|
+
const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
|
|
1362
|
+
if (current === safe) return true;
|
|
1363
|
+
if (ns) el.setAttributeNS(ns, name, safe);
|
|
1364
|
+
else el.setAttribute(name, safe);
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
810
1367
|
|
|
811
1368
|
// src/reactivity/track.ts
|
|
812
1369
|
init_dev();
|
|
813
1370
|
|
|
814
1371
|
// src/reactivity/track-core.ts
|
|
815
|
-
|
|
816
|
-
var _isDev2 = isDev();
|
|
1372
|
+
init_errors();
|
|
817
1373
|
var POOL_MAX = 4096;
|
|
818
1374
|
var nodePool = [];
|
|
819
1375
|
function createNode() {
|
|
@@ -821,6 +1377,7 @@ function createNode() {
|
|
|
821
1377
|
sig: null,
|
|
822
1378
|
sub: null,
|
|
823
1379
|
epoch: 0,
|
|
1380
|
+
depVersion: 0,
|
|
824
1381
|
sigPrev: null,
|
|
825
1382
|
sigNext: null,
|
|
826
1383
|
subPrev: null,
|
|
@@ -905,9 +1462,28 @@ function safeInvoke(sub2) {
|
|
|
905
1462
|
try {
|
|
906
1463
|
sub2();
|
|
907
1464
|
} catch (err) {
|
|
908
|
-
|
|
1465
|
+
const s2 = sub2;
|
|
1466
|
+
reportError(err, { phase: s2._errorPhase ?? "effect", name: s2.__name, node: s2._errorNode });
|
|
909
1467
|
}
|
|
910
1468
|
}
|
|
1469
|
+
function depsChanged(sub2) {
|
|
1470
|
+
let node = sub2.depsHead ?? null;
|
|
1471
|
+
if (node === null) return true;
|
|
1472
|
+
while (node !== null) {
|
|
1473
|
+
const sig = node.sig;
|
|
1474
|
+
if (sig === null) return true;
|
|
1475
|
+
if (sig._d === true && sig._validate !== void 0) {
|
|
1476
|
+
try {
|
|
1477
|
+
sig._validate();
|
|
1478
|
+
} catch {
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (node.depVersion !== sig.__v) return true;
|
|
1483
|
+
node = node.subNext;
|
|
1484
|
+
}
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
911
1487
|
var suspendDepth = 0;
|
|
912
1488
|
var trackingSuspended = false;
|
|
913
1489
|
function suspendTracking() {
|
|
@@ -988,7 +1564,7 @@ function track(effectFn, subscriber) {
|
|
|
988
1564
|
const sub2 = subscriber;
|
|
989
1565
|
return sub2._dispose ?? (sub2._dispose = () => cleanup(subscriber));
|
|
990
1566
|
}
|
|
991
|
-
function reactiveBinding(commit) {
|
|
1567
|
+
function reactiveBinding(commit, ownerNode) {
|
|
992
1568
|
const run = () => {
|
|
993
1569
|
const s2 = subscriber;
|
|
994
1570
|
if (s2._disposed || s2._reentrant) return;
|
|
@@ -1004,13 +1580,17 @@ function reactiveBinding(commit) {
|
|
|
1004
1580
|
subscriber.depsTail = null;
|
|
1005
1581
|
subscriber._epoch = 0;
|
|
1006
1582
|
subscriber._structDirty = false;
|
|
1583
|
+
subscriber._hasComputedDep = false;
|
|
1007
1584
|
subscriber._runEpoch = 0;
|
|
1008
1585
|
subscriber._runs = 0;
|
|
1009
1586
|
subscriber._reentrant = false;
|
|
1010
1587
|
subscriber._disposed = false;
|
|
1588
|
+
subscriber._errorPhase = "binding";
|
|
1589
|
+
subscriber._errorNode = ownerNode;
|
|
1011
1590
|
run();
|
|
1012
1591
|
return subscriber._dispose ?? (subscriber._dispose = () => {
|
|
1013
1592
|
subscriber._disposed = true;
|
|
1593
|
+
subscriber._errorNode = void 0;
|
|
1014
1594
|
cleanup(subscriber);
|
|
1015
1595
|
});
|
|
1016
1596
|
}
|
|
@@ -1022,9 +1602,12 @@ function recordDependency(signal2) {
|
|
|
1022
1602
|
const active = sig.__activeNode ?? null;
|
|
1023
1603
|
if (active !== null && active.sub === sub2) {
|
|
1024
1604
|
active.epoch = epoch;
|
|
1605
|
+
active.depVersion = sig.__v ?? Number.NaN;
|
|
1025
1606
|
return;
|
|
1026
1607
|
}
|
|
1608
|
+
if (sig._validate !== void 0) sub2._hasComputedDep = true;
|
|
1027
1609
|
const node = allocNode(signal2, sub2, epoch);
|
|
1610
|
+
node.depVersion = sig.__v ?? Number.NaN;
|
|
1028
1611
|
node.prevActive = active;
|
|
1029
1612
|
sig.__activeNode = node;
|
|
1030
1613
|
linkSub(sub2, node);
|
|
@@ -1043,9 +1626,10 @@ function cleanup(subscriber) {
|
|
|
1043
1626
|
node = next;
|
|
1044
1627
|
}
|
|
1045
1628
|
}
|
|
1046
|
-
var maxSubscriberRepeats =
|
|
1629
|
+
var maxSubscriberRepeats = 1e3;
|
|
1047
1630
|
var maxDrainIterations = 1e6;
|
|
1048
1631
|
var drainEpoch = 0;
|
|
1632
|
+
var quarantined = /* @__PURE__ */ new Set();
|
|
1049
1633
|
function setMaxSubscriberRepeats(n) {
|
|
1050
1634
|
const prev = maxSubscriberRepeats;
|
|
1051
1635
|
if (Number.isFinite(n) && n > 0) maxSubscriberRepeats = Math.floor(n);
|
|
@@ -1067,19 +1651,21 @@ function tickRepeat(sub2) {
|
|
|
1067
1651
|
return s2._runs > maxSubscriberRepeats;
|
|
1068
1652
|
}
|
|
1069
1653
|
function cycleError(sub2) {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
`
|
|
1074
|
-
)
|
|
1075
|
-
|
|
1654
|
+
const name = sub2.__name ?? "<unnamed>";
|
|
1655
|
+
reportError(
|
|
1656
|
+
new Error(
|
|
1657
|
+
`subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. This subscriber is quarantined for the rest of this update; other pending work still runs.`
|
|
1658
|
+
),
|
|
1659
|
+
{ phase: "scheduler", name }
|
|
1660
|
+
);
|
|
1076
1661
|
}
|
|
1077
1662
|
function absoluteDrainError() {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
`
|
|
1081
|
-
)
|
|
1082
|
-
|
|
1663
|
+
reportError(
|
|
1664
|
+
new Error(
|
|
1665
|
+
`Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
|
|
1666
|
+
),
|
|
1667
|
+
{ phase: "scheduler" }
|
|
1668
|
+
);
|
|
1083
1669
|
}
|
|
1084
1670
|
function drainQueue() {
|
|
1085
1671
|
let i2 = 0;
|
|
@@ -1089,11 +1675,14 @@ function drainQueue() {
|
|
|
1089
1675
|
break;
|
|
1090
1676
|
}
|
|
1091
1677
|
const sub2 = pendingQueue[i2++];
|
|
1678
|
+
pendingSet.delete(sub2);
|
|
1679
|
+
if (quarantined.size > 0 && quarantined.has(sub2)) continue;
|
|
1680
|
+
if (sub2._hasComputedDep === true && !depsChanged(sub2)) continue;
|
|
1092
1681
|
if (tickRepeat(sub2)) {
|
|
1093
1682
|
cycleError(sub2);
|
|
1094
|
-
|
|
1683
|
+
quarantined.add(sub2);
|
|
1684
|
+
continue;
|
|
1095
1685
|
}
|
|
1096
|
-
pendingSet.delete(sub2);
|
|
1097
1686
|
safeInvoke(sub2);
|
|
1098
1687
|
}
|
|
1099
1688
|
}
|
|
@@ -1108,6 +1697,7 @@ function drainNotificationQueue() {
|
|
|
1108
1697
|
if (notifyDepth === 0) {
|
|
1109
1698
|
pendingQueue.length = 0;
|
|
1110
1699
|
pendingSet.clear();
|
|
1700
|
+
if (quarantined.size > 0) quarantined.clear();
|
|
1111
1701
|
}
|
|
1112
1702
|
}
|
|
1113
1703
|
}
|
|
@@ -1201,6 +1791,7 @@ function notifySubscribers(signal2) {
|
|
|
1201
1791
|
if (notifyDepth === 0) {
|
|
1202
1792
|
pendingQueue.length = 0;
|
|
1203
1793
|
pendingSet.clear();
|
|
1794
|
+
if (quarantined.size > 0) quarantined.clear();
|
|
1204
1795
|
}
|
|
1205
1796
|
}
|
|
1206
1797
|
}
|
|
@@ -1228,7 +1819,7 @@ function forEachSubscriber(signal2, visit) {
|
|
|
1228
1819
|
|
|
1229
1820
|
// src/reactivity/track.ts
|
|
1230
1821
|
var _isDev3 = isDev();
|
|
1231
|
-
var _runtimeVersion = true ? "
|
|
1822
|
+
var _runtimeVersion = true ? "4.0.0" : "dev";
|
|
1232
1823
|
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
|
|
1233
1824
|
function resolveReactiveApi() {
|
|
1234
1825
|
const g2 = globalThis;
|
|
@@ -1285,16 +1876,9 @@ var getSubscriberDeps2 = API.getSubscriberDeps;
|
|
|
1285
1876
|
var forEachSubscriber2 = API.forEachSubscriber;
|
|
1286
1877
|
|
|
1287
1878
|
// src/reactivity/bindAttribute.ts
|
|
1288
|
-
var _isDev4 = isDev();
|
|
1289
|
-
function setProp(el, key, val) {
|
|
1290
|
-
el[key] = val;
|
|
1291
|
-
}
|
|
1292
1879
|
function bindAttribute(el, attr, getter) {
|
|
1293
1880
|
if (isEventHandlerAttr(attr)) {
|
|
1294
|
-
|
|
1295
|
-
devWarn(
|
|
1296
|
-
`bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
|
|
1297
|
-
);
|
|
1881
|
+
setSafeAttribute(el, attr, "", { label: `bindAttribute("${attr}")` });
|
|
1298
1882
|
return () => {
|
|
1299
1883
|
};
|
|
1300
1884
|
}
|
|
@@ -1303,97 +1887,19 @@ function bindAttribute(el, attr, getter) {
|
|
|
1303
1887
|
try {
|
|
1304
1888
|
value = getter();
|
|
1305
1889
|
} catch (err) {
|
|
1306
|
-
|
|
1307
|
-
devWarn(`bindAttribute: getter for "${attr}" threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
1890
|
+
reportError(err, { phase: "binding", name: `bindAttribute("${attr}")`, node: el });
|
|
1308
1891
|
return;
|
|
1309
1892
|
}
|
|
1310
|
-
|
|
1311
|
-
if (attr in el && (attr === "checked" || attr === "disabled" || attr === "selected")) {
|
|
1312
|
-
setProp(el, attr, value);
|
|
1313
|
-
} else if (value) {
|
|
1314
|
-
el.setAttribute(attr, "");
|
|
1315
|
-
} else {
|
|
1316
|
-
el.removeAttribute(attr);
|
|
1317
|
-
}
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
const str = String(value);
|
|
1321
|
-
if ((attr === "value" || attr === "checked") && attr in el) {
|
|
1322
|
-
setProp(el, attr, attr === "checked" ? Boolean(value) : str);
|
|
1323
|
-
} else {
|
|
1324
|
-
el.setAttribute(attr, sanitizeAttributeString(attr, str));
|
|
1325
|
-
}
|
|
1893
|
+
setSafeAttribute(el, attr, value, { label: `bindAttribute("${attr}")` });
|
|
1326
1894
|
}
|
|
1327
|
-
return reactiveBinding2(commit);
|
|
1895
|
+
return reactiveBinding2(commit, el);
|
|
1328
1896
|
}
|
|
1329
1897
|
|
|
1330
1898
|
// src/reactivity/bindChildNode.ts
|
|
1331
1899
|
init_dev();
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
init_dev();
|
|
1335
|
-
var elementDisposers = /* @__PURE__ */ new WeakMap();
|
|
1900
|
+
init_errors();
|
|
1901
|
+
init_dispose();
|
|
1336
1902
|
var _isDev5 = isDev();
|
|
1337
|
-
var activeBindingCount = 0;
|
|
1338
|
-
function registerDisposer(node, teardown) {
|
|
1339
|
-
let disposers = elementDisposers.get(node);
|
|
1340
|
-
if (!disposers) {
|
|
1341
|
-
disposers = [];
|
|
1342
|
-
elementDisposers.set(node, disposers);
|
|
1343
|
-
}
|
|
1344
|
-
disposers.push(teardown);
|
|
1345
|
-
if (_isDev5) activeBindingCount++;
|
|
1346
|
-
}
|
|
1347
|
-
function dispose(node) {
|
|
1348
|
-
const stack = [node];
|
|
1349
|
-
const order = [];
|
|
1350
|
-
while (stack.length > 0) {
|
|
1351
|
-
const current = stack.pop();
|
|
1352
|
-
order.push(current);
|
|
1353
|
-
const children = Array.from(current.childNodes);
|
|
1354
|
-
for (let i2 = 0; i2 < children.length; i2++) {
|
|
1355
|
-
stack.push(children[i2]);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
for (let i2 = order.length - 1; i2 >= 0; i2--) {
|
|
1359
|
-
const current = order[i2];
|
|
1360
|
-
const disposers = elementDisposers.get(current);
|
|
1361
|
-
if (disposers) {
|
|
1362
|
-
const snapshot = disposers.slice();
|
|
1363
|
-
elementDisposers.delete(current);
|
|
1364
|
-
if (_isDev5) activeBindingCount -= snapshot.length;
|
|
1365
|
-
for (const d of snapshot) {
|
|
1366
|
-
try {
|
|
1367
|
-
d();
|
|
1368
|
-
} catch (err) {
|
|
1369
|
-
if (_isDev5 && typeof console !== "undefined") {
|
|
1370
|
-
console.warn("[SibuJS] Disposer threw during cleanup:", err);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
let extraPasses = 0;
|
|
1375
|
-
while (extraPasses++ < 8) {
|
|
1376
|
-
const added = elementDisposers.get(current);
|
|
1377
|
-
if (!added || added.length === 0) break;
|
|
1378
|
-
const moreSnapshot = added.slice();
|
|
1379
|
-
elementDisposers.delete(current);
|
|
1380
|
-
if (_isDev5) activeBindingCount -= moreSnapshot.length;
|
|
1381
|
-
for (const d of moreSnapshot) {
|
|
1382
|
-
try {
|
|
1383
|
-
d();
|
|
1384
|
-
} catch (err) {
|
|
1385
|
-
if (_isDev5 && typeof console !== "undefined") {
|
|
1386
|
-
console.warn("[SibuJS] Disposer threw during cleanup:", err);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// src/reactivity/bindChildNode.ts
|
|
1396
|
-
var _isDev6 = isDev();
|
|
1397
1903
|
function bindChildNode(placeholder, getter) {
|
|
1398
1904
|
let lastNodes = [];
|
|
1399
1905
|
function commit() {
|
|
@@ -1401,7 +1907,7 @@ function bindChildNode(placeholder, getter) {
|
|
|
1401
1907
|
try {
|
|
1402
1908
|
result = getter();
|
|
1403
1909
|
} catch (err) {
|
|
1404
|
-
|
|
1910
|
+
reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
|
|
1405
1911
|
return;
|
|
1406
1912
|
}
|
|
1407
1913
|
if (result == null || typeof result === "boolean") {
|
|
@@ -1427,7 +1933,7 @@ function bindChildNode(placeholder, getter) {
|
|
|
1427
1933
|
if (item == null || typeof item === "boolean") continue;
|
|
1428
1934
|
const node = item instanceof Node ? item : document.createTextNode(String(item));
|
|
1429
1935
|
if (seen.has(node)) {
|
|
1430
|
-
if (
|
|
1936
|
+
if (_isDev5)
|
|
1431
1937
|
devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
|
|
1432
1938
|
continue;
|
|
1433
1939
|
}
|
|
@@ -1462,13 +1968,14 @@ function bindChildNode(placeholder, getter) {
|
|
|
1462
1968
|
}
|
|
1463
1969
|
lastNodes = newNodes;
|
|
1464
1970
|
}
|
|
1465
|
-
return reactiveBinding2(commit);
|
|
1971
|
+
return reactiveBinding2(commit, placeholder);
|
|
1466
1972
|
}
|
|
1467
1973
|
|
|
1468
1974
|
// src/core/rendering/tagFactory.ts
|
|
1469
1975
|
init_sanitize();
|
|
1470
|
-
|
|
1471
|
-
var
|
|
1976
|
+
init_dispose();
|
|
1977
|
+
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
1978
|
+
var _isDev6 = isDev();
|
|
1472
1979
|
var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
|
|
1473
1980
|
function isBlockedTag(tag) {
|
|
1474
1981
|
return BLOCKED_TAGS.has(tag.toLowerCase());
|
|
@@ -1486,9 +1993,6 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
|
|
|
1486
1993
|
"self",
|
|
1487
1994
|
"frames"
|
|
1488
1995
|
]);
|
|
1489
|
-
function setProp2(el, key, val) {
|
|
1490
|
-
el[key] = val;
|
|
1491
|
-
}
|
|
1492
1996
|
function looksLikeClassList(s2) {
|
|
1493
1997
|
const t2 = s2.trim();
|
|
1494
1998
|
if (!t2) return false;
|
|
@@ -1512,13 +2016,13 @@ function toKebab(prop) {
|
|
|
1512
2016
|
function applyStyle(el, style2) {
|
|
1513
2017
|
if (typeof style2 === "function") {
|
|
1514
2018
|
const teardown = track2(() => {
|
|
1515
|
-
el.setAttribute("style", style2());
|
|
2019
|
+
el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
|
|
1516
2020
|
});
|
|
1517
2021
|
registerDisposer(el, teardown);
|
|
1518
2022
|
return;
|
|
1519
2023
|
}
|
|
1520
2024
|
if (typeof style2 === "string") {
|
|
1521
|
-
el.setAttribute("style", style2);
|
|
2025
|
+
el.setAttribute("style", sanitizeStyleAttribute(style2));
|
|
1522
2026
|
return;
|
|
1523
2027
|
}
|
|
1524
2028
|
const htmlEl = el;
|
|
@@ -1639,7 +2143,7 @@ var tagFactory = (tag, ns) => {
|
|
|
1639
2143
|
appendChildren(el, second);
|
|
1640
2144
|
return el;
|
|
1641
2145
|
}
|
|
1642
|
-
if (
|
|
2146
|
+
if (_isDev6 && looksLikeClassList(first)) {
|
|
1643
2147
|
devWarn(
|
|
1644
2148
|
`tagFactory: lone string "${first}" looks like a class list but is being rendered as TEXT. For a class, use ${tag}({ class: "${first}" }) \u2014 or ${tag}("${first}", children) to set the class AND add children.`
|
|
1645
2149
|
);
|
|
@@ -1660,7 +2164,7 @@ var tagFactory = (tag, ns) => {
|
|
|
1660
2164
|
if (pClass != null) applyClass(el, pClass);
|
|
1661
2165
|
const pId = props.id;
|
|
1662
2166
|
if (pId != null) {
|
|
1663
|
-
if (
|
|
2167
|
+
if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
|
|
1664
2168
|
devWarn(
|
|
1665
2169
|
`tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
|
|
1666
2170
|
);
|
|
@@ -1675,7 +2179,7 @@ var tagFactory = (tag, ns) => {
|
|
|
1675
2179
|
const handler = pOn[ev];
|
|
1676
2180
|
if (typeof handler === "function") {
|
|
1677
2181
|
el.addEventListener(ev, handler);
|
|
1678
|
-
} else if (
|
|
2182
|
+
} else if (_isDev6) {
|
|
1679
2183
|
devWarn(
|
|
1680
2184
|
`tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
|
|
1681
2185
|
);
|
|
@@ -1703,16 +2207,8 @@ var tagFactory = (tag, ns) => {
|
|
|
1703
2207
|
if (isEventHandlerAttr(key)) continue;
|
|
1704
2208
|
if (typeof value === "function") {
|
|
1705
2209
|
registerDisposer(el, bindAttribute(el, key, value));
|
|
1706
|
-
} else if (typeof value === "boolean") {
|
|
1707
|
-
if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
|
|
1708
|
-
setProp2(el, key, value);
|
|
1709
|
-
} else if (value) {
|
|
1710
|
-
el.setAttribute(key, "");
|
|
1711
|
-
} else {
|
|
1712
|
-
el.removeAttribute(key);
|
|
1713
|
-
}
|
|
1714
2210
|
} else {
|
|
1715
|
-
el
|
|
2211
|
+
setSafeAttribute(el, key, value, { syncValueProperty: false, label: "tagFactory" });
|
|
1716
2212
|
}
|
|
1717
2213
|
}
|
|
1718
2214
|
}
|
|
@@ -1797,7 +2293,7 @@ var param = tagFactory("param");
|
|
|
1797
2293
|
var picture = tagFactory("picture");
|
|
1798
2294
|
var portal = tagFactory("portal");
|
|
1799
2295
|
var source = tagFactory("source");
|
|
1800
|
-
var svg = tagFactory("svg",
|
|
2296
|
+
var svg = tagFactory("svg", SVG_NS2);
|
|
1801
2297
|
var math = tagFactory("math");
|
|
1802
2298
|
var canvas = tagFactory("canvas");
|
|
1803
2299
|
var noscript = tagFactory("noscript");
|
|
@@ -1838,26 +2334,26 @@ var base = tagFactory("base");
|
|
|
1838
2334
|
var link = tagFactory("link");
|
|
1839
2335
|
var meta = tagFactory("meta");
|
|
1840
2336
|
var style = tagFactory("style");
|
|
1841
|
-
var circle = tagFactory("circle",
|
|
1842
|
-
var ellipse = tagFactory("ellipse",
|
|
1843
|
-
var g = tagFactory("g",
|
|
1844
|
-
var line = tagFactory("line",
|
|
1845
|
-
var path = tagFactory("path",
|
|
1846
|
-
var polygon = tagFactory("polygon",
|
|
1847
|
-
var polyline = tagFactory("polyline",
|
|
1848
|
-
var rect = tagFactory("rect",
|
|
1849
|
-
var text = tagFactory("text",
|
|
1850
|
-
var tspan = tagFactory("tspan",
|
|
1851
|
-
var defs = tagFactory("defs",
|
|
1852
|
-
var clipPath = tagFactory("clipPath",
|
|
1853
|
-
var mask = tagFactory("mask",
|
|
1854
|
-
var pattern = tagFactory("pattern",
|
|
1855
|
-
var linearGradient = tagFactory("linearGradient",
|
|
1856
|
-
var radialGradient = tagFactory("radialGradient",
|
|
1857
|
-
var stop = tagFactory("stop",
|
|
1858
|
-
var use = tagFactory("use",
|
|
1859
|
-
var symbol = tagFactory("symbol",
|
|
1860
|
-
var marker = tagFactory("marker",
|
|
2337
|
+
var circle = tagFactory("circle", SVG_NS2);
|
|
2338
|
+
var ellipse = tagFactory("ellipse", SVG_NS2);
|
|
2339
|
+
var g = tagFactory("g", SVG_NS2);
|
|
2340
|
+
var line = tagFactory("line", SVG_NS2);
|
|
2341
|
+
var path = tagFactory("path", SVG_NS2);
|
|
2342
|
+
var polygon = tagFactory("polygon", SVG_NS2);
|
|
2343
|
+
var polyline = tagFactory("polyline", SVG_NS2);
|
|
2344
|
+
var rect = tagFactory("rect", SVG_NS2);
|
|
2345
|
+
var text = tagFactory("text", SVG_NS2);
|
|
2346
|
+
var tspan = tagFactory("tspan", SVG_NS2);
|
|
2347
|
+
var defs = tagFactory("defs", SVG_NS2);
|
|
2348
|
+
var clipPath = tagFactory("clipPath", SVG_NS2);
|
|
2349
|
+
var mask = tagFactory("mask", SVG_NS2);
|
|
2350
|
+
var pattern = tagFactory("pattern", SVG_NS2);
|
|
2351
|
+
var linearGradient = tagFactory("linearGradient", SVG_NS2);
|
|
2352
|
+
var radialGradient = tagFactory("radialGradient", SVG_NS2);
|
|
2353
|
+
var stop = tagFactory("stop", SVG_NS2);
|
|
2354
|
+
var use = tagFactory("use", SVG_NS2);
|
|
2355
|
+
var symbol = tagFactory("symbol", SVG_NS2);
|
|
2356
|
+
var marker = tagFactory("marker", SVG_NS2);
|
|
1861
2357
|
var center = tagFactory("center");
|
|
1862
2358
|
var font = tagFactory("font");
|
|
1863
2359
|
var marquee = tagFactory("marquee");
|
|
@@ -1915,74 +2411,79 @@ var isBatching = API2.isBatching;
|
|
|
1915
2411
|
// src/core/signals/signal.ts
|
|
1916
2412
|
init_dev();
|
|
1917
2413
|
var _g = globalThis;
|
|
1918
|
-
var
|
|
2414
|
+
var _isDev7 = isDev();
|
|
1919
2415
|
function signal(initial, options) {
|
|
1920
|
-
const
|
|
2416
|
+
const state2 = {
|
|
1921
2417
|
value: initial,
|
|
1922
2418
|
__v: 0,
|
|
1923
2419
|
__sc: 0,
|
|
1924
2420
|
subsHead: null,
|
|
1925
2421
|
subsTail: null,
|
|
1926
2422
|
__activeNode: null,
|
|
1927
|
-
__name: void 0
|
|
2423
|
+
__name: void 0,
|
|
2424
|
+
_d: false,
|
|
2425
|
+
_validate: void 0
|
|
1928
2426
|
};
|
|
1929
|
-
const debugName =
|
|
2427
|
+
const debugName = _isDev7 ? options?.name : void 0;
|
|
1930
2428
|
const equalsFn = options?.equals;
|
|
1931
|
-
if (debugName)
|
|
2429
|
+
if (debugName) state2.__name = debugName;
|
|
1932
2430
|
function get() {
|
|
1933
|
-
recordDependency2(
|
|
1934
|
-
return
|
|
2431
|
+
recordDependency2(state2);
|
|
2432
|
+
return state2.value;
|
|
1935
2433
|
}
|
|
1936
|
-
get.__signal =
|
|
2434
|
+
get.__signal = state2;
|
|
1937
2435
|
if (debugName) get.__name = debugName;
|
|
1938
2436
|
let set;
|
|
1939
2437
|
if (equalsFn) {
|
|
1940
2438
|
set = (next) => {
|
|
1941
|
-
const prev =
|
|
2439
|
+
const prev = state2.value;
|
|
1942
2440
|
const newValue = typeof next === "function" ? next(prev) : next;
|
|
1943
2441
|
if (equalsFn(prev, newValue)) return;
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
if (
|
|
2442
|
+
state2.value = newValue;
|
|
2443
|
+
state2.__v++;
|
|
2444
|
+
if (_isDev7) {
|
|
1947
2445
|
const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
|
|
1948
|
-
if (hook) hook.emit("signal:update", { signal:
|
|
2446
|
+
if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
|
|
1949
2447
|
}
|
|
1950
|
-
if (!enqueueBatchedSignal(
|
|
1951
|
-
notifySubscribers2(
|
|
2448
|
+
if (!enqueueBatchedSignal(state2)) {
|
|
2449
|
+
notifySubscribers2(state2);
|
|
1952
2450
|
}
|
|
1953
2451
|
};
|
|
1954
|
-
} else if (
|
|
2452
|
+
} else if (_isDev7) {
|
|
1955
2453
|
set = (next) => {
|
|
1956
|
-
const prev =
|
|
2454
|
+
const prev = state2.value;
|
|
1957
2455
|
const newValue = typeof next === "function" ? next(prev) : next;
|
|
1958
2456
|
if (Object.is(newValue, prev)) return;
|
|
1959
|
-
|
|
1960
|
-
|
|
2457
|
+
state2.value = newValue;
|
|
2458
|
+
state2.__v++;
|
|
1961
2459
|
const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
|
|
1962
|
-
if (hook) hook.emit("signal:update", { signal:
|
|
1963
|
-
if (!enqueueBatchedSignal(
|
|
1964
|
-
notifySubscribers2(
|
|
2460
|
+
if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
|
|
2461
|
+
if (!enqueueBatchedSignal(state2)) {
|
|
2462
|
+
notifySubscribers2(state2);
|
|
1965
2463
|
}
|
|
1966
2464
|
};
|
|
1967
2465
|
} else {
|
|
1968
2466
|
set = (next) => {
|
|
1969
|
-
const prev =
|
|
2467
|
+
const prev = state2.value;
|
|
1970
2468
|
const newValue = typeof next === "function" ? next(prev) : next;
|
|
1971
2469
|
if (Object.is(newValue, prev)) return;
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
if (!enqueueBatchedSignal(
|
|
1975
|
-
notifySubscribers2(
|
|
2470
|
+
state2.value = newValue;
|
|
2471
|
+
state2.__v++;
|
|
2472
|
+
if (!enqueueBatchedSignal(state2)) {
|
|
2473
|
+
notifySubscribers2(state2);
|
|
1976
2474
|
}
|
|
1977
2475
|
};
|
|
1978
2476
|
}
|
|
1979
|
-
if (
|
|
2477
|
+
if (_isDev7) {
|
|
1980
2478
|
const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
|
|
1981
|
-
if (hook) hook.emit("signal:create", { signal:
|
|
2479
|
+
if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
|
|
1982
2480
|
}
|
|
1983
2481
|
return [get, set];
|
|
1984
2482
|
}
|
|
1985
2483
|
|
|
2484
|
+
// src/plugins/i18n.ts
|
|
2485
|
+
init_ssr_context();
|
|
2486
|
+
|
|
1986
2487
|
// src/utils/globalSingleton.ts
|
|
1987
2488
|
function globalSingleton(key, create) {
|
|
1988
2489
|
const g2 = globalThis;
|
|
@@ -1992,37 +2493,65 @@ function globalSingleton(key, create) {
|
|
|
1992
2493
|
// src/plugins/i18n.ts
|
|
1993
2494
|
var _i18n = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.i18n.v1"), () => ({
|
|
1994
2495
|
locale: signal("en"),
|
|
1995
|
-
locales:
|
|
2496
|
+
locales: /* @__PURE__ */ Object.create(null)
|
|
1996
2497
|
}));
|
|
1997
|
-
var [
|
|
2498
|
+
var [clientLocale, setClientLocale] = _i18n.locale;
|
|
1998
2499
|
var locales = _i18n.locales;
|
|
2500
|
+
function dictionaryFor(locale) {
|
|
2501
|
+
return Object.hasOwn(locales, locale) ? locales[locale] : void 0;
|
|
2502
|
+
}
|
|
2503
|
+
function lookup(key) {
|
|
2504
|
+
const dictionary = dictionaryFor(getLocale());
|
|
2505
|
+
if (dictionary === void 0 || !Object.hasOwn(dictionary, key)) return void 0;
|
|
2506
|
+
const message = dictionary[key];
|
|
2507
|
+
return typeof message === "string" ? message : void 0;
|
|
2508
|
+
}
|
|
1999
2509
|
function setLocale(locale) {
|
|
2000
|
-
|
|
2510
|
+
const request = getRequestStore();
|
|
2511
|
+
if (request) {
|
|
2512
|
+
request.locale = locale;
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
setClientLocale(locale);
|
|
2001
2516
|
}
|
|
2002
2517
|
function getLocale() {
|
|
2003
|
-
|
|
2518
|
+
const request = getRequestStore();
|
|
2519
|
+
if (request) {
|
|
2520
|
+
return request.locale ?? clientLocale();
|
|
2521
|
+
}
|
|
2522
|
+
return clientLocale();
|
|
2004
2523
|
}
|
|
2005
2524
|
function registerTranslations(locale, messages) {
|
|
2006
|
-
|
|
2525
|
+
const prepared = { ...messages };
|
|
2526
|
+
const merged = Object.assign(/* @__PURE__ */ Object.create(null), dictionaryFor(locale), prepared);
|
|
2527
|
+
Object.defineProperty(locales, locale, {
|
|
2528
|
+
value: merged,
|
|
2529
|
+
writable: true,
|
|
2530
|
+
enumerable: true,
|
|
2531
|
+
configurable: true
|
|
2532
|
+
});
|
|
2007
2533
|
}
|
|
2008
2534
|
function t(key, params) {
|
|
2009
|
-
const
|
|
2010
|
-
const message = locales[locale]?.[key] || key;
|
|
2535
|
+
const message = lookup(key) ?? key;
|
|
2011
2536
|
return params ? message.replace(/\{(\w+)\}/g, (_, p2) => String(params[p2] ?? "")) : message;
|
|
2012
2537
|
}
|
|
2013
2538
|
function Trans(key, params) {
|
|
2014
2539
|
return span(() => t(key, params));
|
|
2015
2540
|
}
|
|
2016
2541
|
function hasTranslation(key) {
|
|
2017
|
-
|
|
2018
|
-
return key in (locales[locale] || {});
|
|
2542
|
+
return lookup(key) !== void 0;
|
|
2019
2543
|
}
|
|
2020
2544
|
function getAvailableLocales() {
|
|
2021
2545
|
return Object.keys(locales);
|
|
2022
2546
|
}
|
|
2023
2547
|
|
|
2548
|
+
// src/plugins/router.ts
|
|
2549
|
+
init_dev();
|
|
2550
|
+
init_dispose();
|
|
2551
|
+
|
|
2024
2552
|
// src/core/signals/effect.ts
|
|
2025
2553
|
init_dev();
|
|
2554
|
+
init_errors();
|
|
2026
2555
|
init_ssr_context();
|
|
2027
2556
|
var _g2 = globalThis;
|
|
2028
2557
|
var MAX_RERUNS = 100;
|
|
@@ -2034,7 +2563,7 @@ function flushUserCleanups(ctx) {
|
|
|
2034
2563
|
try {
|
|
2035
2564
|
list[i2]();
|
|
2036
2565
|
} catch (err) {
|
|
2037
|
-
|
|
2566
|
+
reportError(err, { phase: "cleanup" });
|
|
2038
2567
|
}
|
|
2039
2568
|
}
|
|
2040
2569
|
}
|
|
@@ -2047,11 +2576,12 @@ function drainReruns(ctx) {
|
|
|
2047
2576
|
} while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
|
|
2048
2577
|
if (ctx.rerunPending) {
|
|
2049
2578
|
ctx.rerunPending = false;
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
`
|
|
2053
|
-
)
|
|
2054
|
-
|
|
2579
|
+
reportError(
|
|
2580
|
+
new Error(
|
|
2581
|
+
`effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
|
|
2582
|
+
),
|
|
2583
|
+
{ phase: "effect" }
|
|
2584
|
+
);
|
|
2055
2585
|
}
|
|
2056
2586
|
}
|
|
2057
2587
|
function disposeEffect(ctx) {
|
|
@@ -2128,6 +2658,8 @@ function effect(effectFn, options) {
|
|
|
2128
2658
|
sub2.depsTail = null;
|
|
2129
2659
|
sub2._epoch = 0;
|
|
2130
2660
|
sub2._structDirty = false;
|
|
2661
|
+
sub2._hasComputedDep = false;
|
|
2662
|
+
sub2._errorPhase = "effect";
|
|
2131
2663
|
sub2._runEpoch = 0;
|
|
2132
2664
|
sub2._runs = 0;
|
|
2133
2665
|
ctx.subscriber = sub2;
|
|
@@ -2146,12 +2678,32 @@ function effect(effectFn, options) {
|
|
|
2146
2678
|
|
|
2147
2679
|
// src/plugins/router.ts
|
|
2148
2680
|
init_sanitize();
|
|
2149
|
-
function
|
|
2150
|
-
if (path2 === "") return
|
|
2681
|
+
function classifyNavigationTarget(path2) {
|
|
2682
|
+
if (path2 === "") return "internal";
|
|
2151
2683
|
const normalized = stripControlChars(path2).replace(/\\/g, "/");
|
|
2152
|
-
if (normalized
|
|
2153
|
-
|
|
2684
|
+
if (normalized === "") return "internal";
|
|
2685
|
+
if (normalized.startsWith("//")) return "unsafe";
|
|
2686
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(normalized)) return "internal";
|
|
2687
|
+
return sanitizeUrl(normalized) === "" ? "unsafe" : "external";
|
|
2688
|
+
}
|
|
2689
|
+
function isRouterNavigable(path2) {
|
|
2690
|
+
return classifyNavigationTarget(path2) === "internal";
|
|
2691
|
+
}
|
|
2692
|
+
function normalizePathname(path2) {
|
|
2693
|
+
if (!path2) return "/";
|
|
2694
|
+
const trimmed = path2.length > 1 ? path2.replace(/\/+$/, "") : path2;
|
|
2695
|
+
return trimmed || "/";
|
|
2154
2696
|
}
|
|
2697
|
+
function normalizeQuery(query) {
|
|
2698
|
+
const params = new URLSearchParams(query);
|
|
2699
|
+
return [...params.entries()].sort(([a2], [b2]) => a2 < b2 ? -1 : a2 > b2 ? 1 : 0).map(([k, v]) => `${k}=${v}`).join("&");
|
|
2700
|
+
}
|
|
2701
|
+
function isPathnameAncestor(target, current) {
|
|
2702
|
+
if (target === current) return true;
|
|
2703
|
+
if (target === "/") return false;
|
|
2704
|
+
return current.startsWith(`${target}/`);
|
|
2705
|
+
}
|
|
2706
|
+
var ABORT_ROUTER_DESTROYED = "sibu:router-destroyed";
|
|
2155
2707
|
var LRUCache = class {
|
|
2156
2708
|
constructor(maxSize = 100) {
|
|
2157
2709
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -2212,9 +2764,9 @@ var NavigationController = class {
|
|
|
2212
2764
|
}
|
|
2213
2765
|
}
|
|
2214
2766
|
}
|
|
2215
|
-
abort() {
|
|
2767
|
+
abort(reason) {
|
|
2216
2768
|
if (this.currentController) {
|
|
2217
|
-
this.currentController.abort();
|
|
2769
|
+
this.currentController.abort(reason);
|
|
2218
2770
|
this.currentController = null;
|
|
2219
2771
|
}
|
|
2220
2772
|
}
|
|
@@ -2400,6 +2952,7 @@ var GuardManager = class {
|
|
|
2400
2952
|
this.timeout = timeout;
|
|
2401
2953
|
}
|
|
2402
2954
|
async runBeforeEach(to, from, signal2) {
|
|
2955
|
+
if (signal2.aborted) throw new Error("Navigation aborted");
|
|
2403
2956
|
for (const guard of this.beforeEachGuards) {
|
|
2404
2957
|
if (signal2.aborted) throw new Error("Navigation aborted");
|
|
2405
2958
|
const result = await this.runGuard(guard, to, from, signal2);
|
|
@@ -2408,6 +2961,7 @@ var GuardManager = class {
|
|
|
2408
2961
|
return true;
|
|
2409
2962
|
}
|
|
2410
2963
|
async runBeforeResolve(to, from, signal2) {
|
|
2964
|
+
if (signal2.aborted) throw new Error("Navigation aborted");
|
|
2411
2965
|
for (const guard of this.beforeResolveGuards) {
|
|
2412
2966
|
if (signal2.aborted) throw new Error("Navigation aborted");
|
|
2413
2967
|
const result = await this.runGuard(guard, to, from, signal2);
|
|
@@ -2504,13 +3058,16 @@ var _ComponentLoader = class _ComponentLoader {
|
|
|
2504
3058
|
constructor(cacheSize = 50, retryDelay = 1e3) {
|
|
2505
3059
|
this.errorCache = /* @__PURE__ */ new Map();
|
|
2506
3060
|
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
3061
|
+
// In-flight *module* resolutions for deferred plans, so concurrent first
|
|
3062
|
+
// mounts of a lazy route do not each run the loader arrow.
|
|
3063
|
+
this.moduleLoads = /* @__PURE__ */ new Map();
|
|
2507
3064
|
// Stable per-route-definition id. Caching by the RESOLVED path (e.g.
|
|
2508
3065
|
// /users/123) gave the cache one entry per visited URL, so parameterized
|
|
2509
3066
|
// routes thrashed/evicted and the component was reloaded every navigation.
|
|
2510
3067
|
// Keying by route-definition identity makes the cache effective again.
|
|
2511
3068
|
this.routeKeys = /* @__PURE__ */ new WeakMap();
|
|
2512
3069
|
this.keyCounter = 0;
|
|
2513
|
-
this.
|
|
3070
|
+
this.planCache = new LRUCache(cacheSize);
|
|
2514
3071
|
this.retryDelay = retryDelay;
|
|
2515
3072
|
}
|
|
2516
3073
|
keyFor(route2) {
|
|
@@ -2521,91 +3078,233 @@ var _ComponentLoader = class _ComponentLoader {
|
|
|
2521
3078
|
}
|
|
2522
3079
|
return key;
|
|
2523
3080
|
}
|
|
2524
|
-
|
|
3081
|
+
/**
|
|
3082
|
+
* Resolve the route's **plan** — how to produce an instance — without ever
|
|
3083
|
+
* instantiating a component to inspect it.
|
|
3084
|
+
*
|
|
3085
|
+
* Loading a route component and instantiating one are separate operations.
|
|
3086
|
+
* The loader never invokes a user component factory merely to validate its
|
|
3087
|
+
* return type; validation happens on the instance that will actually be
|
|
3088
|
+
* mounted. (LOAD-001)
|
|
3089
|
+
*/
|
|
3090
|
+
async loadPlan(route2, routePath) {
|
|
2525
3091
|
if (!("component" in route2)) {
|
|
2526
3092
|
throw new Error(`Route ${routePath} does not have a component`);
|
|
2527
3093
|
}
|
|
2528
3094
|
const comp = route2.component;
|
|
2529
3095
|
const cacheKey = this.keyFor(route2);
|
|
2530
|
-
const cached = this.
|
|
3096
|
+
const cached = this.planCache.get(cacheKey);
|
|
2531
3097
|
if (cached) return cached;
|
|
2532
3098
|
const existingPromise = this.loadingPromises.get(cacheKey);
|
|
2533
3099
|
if (existingPromise) return existingPromise;
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
|
|
2537
|
-
}
|
|
2538
|
-
const loadingPromise = this.doLoadComponent(comp, routePath);
|
|
3100
|
+
this.assertNotHot(cacheKey);
|
|
3101
|
+
const loadingPromise = this.doLoadPlan(comp, routePath);
|
|
2539
3102
|
this.loadingPromises.set(cacheKey, loadingPromise);
|
|
2540
3103
|
try {
|
|
2541
|
-
const
|
|
2542
|
-
this.
|
|
3104
|
+
const plan = await loadingPromise;
|
|
3105
|
+
this.planCache.set(cacheKey, plan);
|
|
2543
3106
|
this.errorCache.delete(cacheKey);
|
|
2544
|
-
return
|
|
3107
|
+
return plan;
|
|
2545
3108
|
} catch (error) {
|
|
2546
|
-
|
|
2547
|
-
if (!this.errorCache.has(cacheKey) && this.errorCache.size >= _ComponentLoader.MAX_ERROR_ENTRIES) {
|
|
2548
|
-
const oldest = this.errorCache.keys().next().value;
|
|
2549
|
-
if (oldest !== void 0) this.errorCache.delete(oldest);
|
|
2550
|
-
}
|
|
2551
|
-
this.errorCache.set(cacheKey, {
|
|
2552
|
-
timestamp: Date.now(),
|
|
2553
|
-
count: currentError.count + 1
|
|
2554
|
-
});
|
|
3109
|
+
this.noteError(cacheKey);
|
|
2555
3110
|
throw error;
|
|
2556
3111
|
} finally {
|
|
2557
3112
|
this.loadingPromises.delete(cacheKey);
|
|
2558
3113
|
}
|
|
2559
3114
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
3115
|
+
/** Reject early while a recent load failure for this route is still hot. */
|
|
3116
|
+
assertNotHot(cacheKey) {
|
|
3117
|
+
const errorInfo = this.errorCache.get(cacheKey);
|
|
3118
|
+
if (errorInfo && Date.now() - errorInfo.timestamp < this.retryDelay) {
|
|
3119
|
+
throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* Record a load failure. Keyed by route-definition id, so the map is bounded
|
|
3124
|
+
* by the number of route definitions; still capped defensively, evicting the
|
|
3125
|
+
* oldest.
|
|
3126
|
+
*/
|
|
3127
|
+
noteError(cacheKey) {
|
|
3128
|
+
const currentError = this.errorCache.get(cacheKey) || { timestamp: 0, count: 0 };
|
|
3129
|
+
if (!this.errorCache.has(cacheKey) && this.errorCache.size >= _ComponentLoader.MAX_ERROR_ENTRIES) {
|
|
3130
|
+
const oldest = this.errorCache.keys().next().value;
|
|
3131
|
+
if (oldest !== void 0) this.errorCache.delete(oldest);
|
|
2567
3132
|
}
|
|
3133
|
+
this.errorCache.set(cacheKey, { timestamp: Date.now(), count: currentError.count + 1 });
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Classify without instantiating.
|
|
3137
|
+
*
|
|
3138
|
+
* The `lazy()` brand decides, and nothing else: a branded loader is
|
|
3139
|
+
* `deferred`, everything else is a `factory`. No inspection of the function's
|
|
3140
|
+
* type or source is involved, so an unbranded factory can never become
|
|
3141
|
+
* preload-executable. (LOAD-004)
|
|
3142
|
+
*
|
|
3143
|
+
* An unbranded factory that happens to resolve to a module — an un-marked
|
|
3144
|
+
* `async () => ({ default: Page })` — still works: that is unwrapped during
|
|
3145
|
+
* real instantiation, where the produced value is used rather than discarded.
|
|
3146
|
+
*/
|
|
3147
|
+
async doLoadPlan(comp, routePath) {
|
|
3148
|
+
if (typeof comp !== "function") {
|
|
3149
|
+
throw new Error(`Component for route "${routePath}" must be a function, got ${typeof comp}`);
|
|
3150
|
+
}
|
|
3151
|
+
if (!this.isDeferredModule(comp)) {
|
|
3152
|
+
return { kind: "factory", component: comp };
|
|
3153
|
+
}
|
|
3154
|
+
return { kind: "deferred", load: comp };
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Create **one** real route component instance, validating what will actually
|
|
3158
|
+
* be mounted.
|
|
3159
|
+
*
|
|
3160
|
+
* `route` is used only to upgrade a `deferred` plan once a lazy module has
|
|
3161
|
+
* resolved, so the import happens once and later mounts call the module's
|
|
3162
|
+
* factory directly.
|
|
3163
|
+
*/
|
|
3164
|
+
async instantiate(plan, route2, routePath) {
|
|
3165
|
+
if (plan.kind === "factory") {
|
|
3166
|
+
return this.instantiateFactory(plan.component, route2, routePath);
|
|
3167
|
+
}
|
|
3168
|
+
const factory = await this.resolveModuleFactory(plan, route2, routePath);
|
|
3169
|
+
return this.instantiateFactory(factory, route2, routePath);
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Resolve a deferred plan's module and cache the factory it exports —
|
|
3173
|
+
* **without invoking that factory**.
|
|
3174
|
+
*
|
|
3175
|
+
* Shared by instantiation and preloading, so a preload racing a navigation to
|
|
3176
|
+
* the same route imports the module once. A deferred plan is only ever a
|
|
3177
|
+
* module loader (see `isDeferredModule`), so a resolution that is not
|
|
3178
|
+
* module-shaped is a configuration error and is reported as one.
|
|
3179
|
+
*/
|
|
3180
|
+
async resolveModuleFactory(plan, route2, routePath) {
|
|
3181
|
+
const cacheKey = this.keyFor(route2);
|
|
3182
|
+
const cached = this.planCache.get(cacheKey);
|
|
3183
|
+
if (cached && cached.kind === "factory") return cached.component;
|
|
3184
|
+
this.assertNotHot(cacheKey);
|
|
3185
|
+
let pending = this.moduleLoads.get(cacheKey);
|
|
3186
|
+
const owner = pending === void 0;
|
|
3187
|
+
if (pending === void 0) {
|
|
3188
|
+
pending = plan.load();
|
|
3189
|
+
this.moduleLoads.set(cacheKey, pending);
|
|
3190
|
+
}
|
|
3191
|
+
let resolved;
|
|
2568
3192
|
try {
|
|
2569
|
-
|
|
2570
|
-
const component = this.extractComponent(result, routePath);
|
|
2571
|
-
const testElement = component();
|
|
2572
|
-
if (!this.isElement(testElement)) {
|
|
2573
|
-
throw new Error(`Component for route "${routePath}" must return Element, got ${typeof testElement}`);
|
|
2574
|
-
}
|
|
2575
|
-
return component;
|
|
3193
|
+
resolved = await pending;
|
|
2576
3194
|
} catch (error) {
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
throw wrapped;
|
|
3195
|
+
this.noteError(cacheKey);
|
|
3196
|
+
throw error;
|
|
3197
|
+
} finally {
|
|
3198
|
+
if (owner) this.moduleLoads.delete(cacheKey);
|
|
2582
3199
|
}
|
|
3200
|
+
const factory = this.extractFactory(resolved, routePath);
|
|
3201
|
+
this.planCache.set(cacheKey, { kind: "factory", component: factory });
|
|
3202
|
+
return factory;
|
|
2583
3203
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
3204
|
+
/**
|
|
3205
|
+
* Invoke a factory exactly once and validate the instance it produced.
|
|
3206
|
+
*
|
|
3207
|
+
* A factory may turn out to resolve to a *module* rather than an Element —
|
|
3208
|
+
* an un-marked `async () => ({ default: Page })` is indistinguishable from a
|
|
3209
|
+
* component factory until it runs. That is settled here, on the real
|
|
3210
|
+
* invocation, and the plan is upgraded so later mounts skip the extra hop.
|
|
3211
|
+
* `depth` bounds that unwrapping; a module whose default is itself a module
|
|
3212
|
+
* is a configuration error, not something to chase.
|
|
3213
|
+
*/
|
|
3214
|
+
async instantiateFactory(component, route2, routePath, depth = 0) {
|
|
3215
|
+
const result = component();
|
|
3216
|
+
const value = this.isThenable(result) ? await result : result;
|
|
3217
|
+
if (this.isElement(value)) return value;
|
|
3218
|
+
if (depth === 0 && this.isFactoryLike(value)) {
|
|
3219
|
+
const factory = this.extractFactory(value, routePath);
|
|
3220
|
+
this.planCache.set(this.keyFor(route2), { kind: "factory", component: factory });
|
|
3221
|
+
return this.instantiateFactory(factory, route2, routePath, depth + 1);
|
|
3222
|
+
}
|
|
3223
|
+
throw new Error(`Component for route "${routePath}" must return Element, got ${typeof value}`);
|
|
2586
3224
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
3225
|
+
/**
|
|
3226
|
+
* Something `extractFactory` can turn into a component factory: a bare
|
|
3227
|
+
* callable, or a module namespace carrying a callable `default`. Both are
|
|
3228
|
+
* long-supported shapes for an async loader's resolution.
|
|
3229
|
+
*/
|
|
3230
|
+
isFactoryLike(value) {
|
|
3231
|
+
if (typeof value === "function") return true;
|
|
3232
|
+
return typeof value === "object" && value !== null && typeof value.default === "function";
|
|
2589
3233
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
3234
|
+
/**
|
|
3235
|
+
* Pull a factory out of a resolved lazy module **without calling it**. Only
|
|
3236
|
+
* what can be checked without running user code is checked here: that a
|
|
3237
|
+
* default export exists and is callable.
|
|
3238
|
+
*/
|
|
3239
|
+
extractFactory(result, routePath) {
|
|
3240
|
+
if (typeof result === "function") return result;
|
|
3241
|
+
if (typeof result === "object" && result !== null && typeof result.default === "function") {
|
|
2592
3242
|
return result.default;
|
|
2593
3243
|
}
|
|
2594
|
-
if (typeof result === "function") {
|
|
2595
|
-
return result;
|
|
2596
|
-
}
|
|
2597
|
-
if (this.isElement(result)) {
|
|
2598
|
-
return () => result;
|
|
2599
|
-
}
|
|
2600
3244
|
throw new Error(`Invalid component module for route "${routePath}"`);
|
|
2601
3245
|
}
|
|
3246
|
+
/**
|
|
3247
|
+
* Prepare explicitly deferred route code without creating a component
|
|
3248
|
+
* instance or any DOM.
|
|
3249
|
+
*
|
|
3250
|
+
* Only routes explicitly branded as deferred — a loader wrapped in `lazy()` —
|
|
3251
|
+
* are resolved during preload. Direct component factories, whether
|
|
3252
|
+
* synchronous, `async`, or Promise-returning, are `factory` plans and
|
|
3253
|
+
* therefore require no preload work at all.
|
|
3254
|
+
*
|
|
3255
|
+
* ```text
|
|
3256
|
+
* factory plan → preload no-op
|
|
3257
|
+
* deferred plan → resolve module → cache factory → no component invocation
|
|
3258
|
+
* ```
|
|
3259
|
+
*
|
|
3260
|
+
* Deferred module resolution caches the component factory the module exports
|
|
3261
|
+
* **without invoking it**. (LOAD-004)
|
|
3262
|
+
*/
|
|
3263
|
+
async preloadPlan(route2, routePath) {
|
|
3264
|
+
const plan = await this.loadPlan(route2, routePath);
|
|
3265
|
+
if (plan.kind === "factory") return;
|
|
3266
|
+
await this.resolveModuleFactory(plan, route2, routePath);
|
|
3267
|
+
}
|
|
3268
|
+
isThenable(value) {
|
|
3269
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Is this route component an **explicitly declared module loader**?
|
|
3273
|
+
*
|
|
3274
|
+
* Preload is the only place SibuJS executes a route function before the
|
|
3275
|
+
* router needs an instance, so permission to do that must be explicit. The
|
|
3276
|
+
* brand stamped by `lazy()` is the sole authority.
|
|
3277
|
+
*
|
|
3278
|
+
* Everything the loader once inferred is deliberately *not* a signal:
|
|
3279
|
+
*
|
|
3280
|
+
* - `constructor.name === "AsyncFunction"` — cannot distinguish
|
|
3281
|
+
* `async () => import("./Page")` from `async () => div()`. Treating the
|
|
3282
|
+
* latter as a module loader is what made preload invoke user
|
|
3283
|
+
* components. (LOAD-003)
|
|
3284
|
+
* - `toString().includes("import(")` — source text is a representation, not
|
|
3285
|
+
* semantics. An ordinary component with `"import('./x')"` in a string, a
|
|
3286
|
+
* template literal, or a comment was executed during preload; and the same
|
|
3287
|
+
* text can vanish under a bundler that rewrites dynamic imports into its
|
|
3288
|
+
* own chunk-loader calls. Both directions are wrong, and no better regex
|
|
3289
|
+
* fixes that — the defect is source inspection itself. (LOAD-004)
|
|
3290
|
+
*
|
|
3291
|
+
* A brand is metadata: it survives TypeScript, every bundler, and
|
|
3292
|
+
* minification, because it is part of runtime behaviour rather than syntax.
|
|
3293
|
+
*/
|
|
3294
|
+
isDeferredModule(comp) {
|
|
3295
|
+
return comp[LAZY_MARKER] === true;
|
|
3296
|
+
}
|
|
3297
|
+
isElement(value) {
|
|
3298
|
+
return value instanceof Element;
|
|
3299
|
+
}
|
|
2602
3300
|
clearErrors() {
|
|
2603
3301
|
this.errorCache.clear();
|
|
2604
3302
|
}
|
|
2605
3303
|
clearCache() {
|
|
2606
|
-
this.
|
|
3304
|
+
this.planCache.clear();
|
|
2607
3305
|
this.errorCache.clear();
|
|
2608
3306
|
this.loadingPromises.clear();
|
|
3307
|
+
this.moduleLoads.clear();
|
|
2609
3308
|
}
|
|
2610
3309
|
};
|
|
2611
3310
|
_ComponentLoader.MAX_ERROR_ENTRIES = 256;
|
|
@@ -2614,6 +3313,17 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2614
3313
|
constructor(routes, options = {}) {
|
|
2615
3314
|
// Event listeners cleanup
|
|
2616
3315
|
this.cleanup = [];
|
|
3316
|
+
// Set by destroy(). Lets a navigation still in flight report
|
|
3317
|
+
// "router-destroyed" rather than the generic "superseded".
|
|
3318
|
+
this.destroyed = false;
|
|
3319
|
+
// Monotonic navigation generation. Bumped by every committed navigation that
|
|
3320
|
+
// can acquire DOM ownership, and by destroy(). Async work that wants to
|
|
3321
|
+
// commit into the DOM captures this first and re-checks before committing:
|
|
3322
|
+
// unlike a URL comparison it distinguishes "/b" generation 10 from "/b"
|
|
3323
|
+
// generation 12, so an A→B→A round trip can never restore commit permission
|
|
3324
|
+
// to superseded work. The router's own initial location resolution is
|
|
3325
|
+
// excluded — it establishes the bootstrap route rather than superseding it.
|
|
3326
|
+
this.navEpoch = 0;
|
|
2617
3327
|
this.options = {
|
|
2618
3328
|
mode: "history",
|
|
2619
3329
|
base: "",
|
|
@@ -2651,7 +3361,9 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2651
3361
|
}
|
|
2652
3362
|
}
|
|
2653
3363
|
queueMicrotask(() => {
|
|
2654
|
-
this.
|
|
3364
|
+
if (!this.navigator.isNavigating) {
|
|
3365
|
+
this.handleLocationChange(true, true);
|
|
3366
|
+
}
|
|
2655
3367
|
this.isReadySetter(true);
|
|
2656
3368
|
});
|
|
2657
3369
|
}
|
|
@@ -2665,14 +3377,15 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2665
3377
|
matched: []
|
|
2666
3378
|
};
|
|
2667
3379
|
}
|
|
2668
|
-
handleLocationChange(skipHistory = true) {
|
|
3380
|
+
handleLocationChange(skipHistory = true, initialResolution = false) {
|
|
2669
3381
|
const path2 = this.getCurrentPath();
|
|
2670
|
-
this.navigate(path2, { replace: true, skipHistory }).catch((err) => {
|
|
3382
|
+
this.navigate(path2, { replace: true, skipHistory, initialResolution }).catch((err) => {
|
|
2671
3383
|
console.error("[Router] Error during location change navigation:", err);
|
|
2672
3384
|
});
|
|
2673
3385
|
}
|
|
2674
3386
|
getCurrentPath() {
|
|
2675
3387
|
const { mode, base: base2 } = this.options;
|
|
3388
|
+
if (typeof window === "undefined") return "/";
|
|
2676
3389
|
if (mode === "hash") {
|
|
2677
3390
|
return window.location.hash.slice(1) || "/";
|
|
2678
3391
|
}
|
|
@@ -2704,15 +3417,15 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2704
3417
|
try {
|
|
2705
3418
|
await this.navigator.navigate(async (signal2) => {
|
|
2706
3419
|
const targetPath = this.resolvePath(to);
|
|
2707
|
-
if (!
|
|
3420
|
+
if (!isRouterNavigable(targetPath)) {
|
|
2708
3421
|
const from2 = this.currentRouteGetter();
|
|
2709
3422
|
const toContext2 = this.createRouteContext(targetPath);
|
|
2710
|
-
throw new NavigationFailureError("aborted", from2, toContext2);
|
|
3423
|
+
throw new NavigationFailureError("aborted", from2, toContext2, void 0, "unsafe-target");
|
|
2711
3424
|
}
|
|
2712
3425
|
const from = this.currentRouteGetter();
|
|
2713
3426
|
const toContext = this.createRouteContext(targetPath);
|
|
2714
3427
|
if (this.isSameRoute(from, toContext)) {
|
|
2715
|
-
throw new NavigationFailureError("duplicated", from, toContext);
|
|
3428
|
+
throw new NavigationFailureError("duplicated", from, toContext, void 0, "duplicate");
|
|
2716
3429
|
}
|
|
2717
3430
|
await this.performNavigation(toContext, from, options, signal2);
|
|
2718
3431
|
});
|
|
@@ -2720,30 +3433,43 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2720
3433
|
} catch (error) {
|
|
2721
3434
|
if (error instanceof NavigationFailureError) {
|
|
2722
3435
|
const failure2 = error.toFailure();
|
|
2723
|
-
return { success: false, type: failure2.type, failure: failure2 };
|
|
3436
|
+
return { success: false, type: failure2.type, reason: failure2.reason, failure: failure2 };
|
|
2724
3437
|
}
|
|
3438
|
+
const isAbort = error instanceof Error && error.message === "Navigation aborted";
|
|
3439
|
+
const reason = isAbort ? this.destroyed ? "router-destroyed" : "superseded" : "error";
|
|
2725
3440
|
const failure = {
|
|
2726
3441
|
type: "aborted",
|
|
3442
|
+
reason,
|
|
2727
3443
|
from: this.currentRouteGetter(),
|
|
2728
3444
|
to: this.createRouteContext(this.resolvePath(to)),
|
|
2729
3445
|
error: error instanceof Error ? error : new Error(String(error))
|
|
2730
3446
|
};
|
|
2731
|
-
return { success: false, type: failure.type, failure };
|
|
3447
|
+
return { success: false, type: failure.type, reason, failure };
|
|
2732
3448
|
}
|
|
2733
3449
|
}
|
|
2734
|
-
async performNavigation(to, from, options, signal2, depth = 0) {
|
|
3450
|
+
async performNavigation(to, from, options, signal2, depth = 0, redirectChain = []) {
|
|
3451
|
+
const chain = depth === 0 ? [to.path] : redirectChain;
|
|
2735
3452
|
if (depth >= _SibuRouter.MAX_REDIRECT_DEPTH) {
|
|
2736
|
-
|
|
3453
|
+
if (isDev()) {
|
|
3454
|
+
devWarn(
|
|
3455
|
+
`Router: redirect loop detected \u2014 navigation aborted after ${_SibuRouter.MAX_REDIRECT_DEPTH} hops.
|
|
3456
|
+
${chain.map((p2) => `"${p2}"`).join(" -> ")}`
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "redirect-loop");
|
|
2737
3460
|
}
|
|
2738
3461
|
const beforeEachResult = await this.guards.runBeforeEach(to, from, signal2);
|
|
2739
3462
|
if (beforeEachResult !== true) {
|
|
2740
3463
|
if (typeof beforeEachResult === "string") {
|
|
2741
|
-
if (!
|
|
2742
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
3464
|
+
if (!isRouterNavigable(beforeEachResult)) {
|
|
3465
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
2743
3466
|
}
|
|
2744
|
-
return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1
|
|
3467
|
+
return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1, [
|
|
3468
|
+
...chain,
|
|
3469
|
+
beforeEachResult
|
|
3470
|
+
]);
|
|
2745
3471
|
}
|
|
2746
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
3472
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "guard");
|
|
2747
3473
|
}
|
|
2748
3474
|
const match = this.matcher.match(to.path);
|
|
2749
3475
|
if (match) {
|
|
@@ -2754,47 +3480,60 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2754
3480
|
for (const guard of guards) {
|
|
2755
3481
|
if (signal2.aborted) throw new Error("Navigation aborted");
|
|
2756
3482
|
const result = await guard(to, from);
|
|
3483
|
+
if (signal2.aborted) {
|
|
3484
|
+
throw new NavigationFailureError("aborted", from, to, void 0, abortReasonOf(signal2));
|
|
3485
|
+
}
|
|
2757
3486
|
if (result !== true) {
|
|
2758
3487
|
if (typeof result === "string") {
|
|
2759
|
-
if (!
|
|
2760
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
3488
|
+
if (!isRouterNavigable(result)) {
|
|
3489
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
2761
3490
|
}
|
|
2762
|
-
return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1
|
|
3491
|
+
return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1, [
|
|
3492
|
+
...chain,
|
|
3493
|
+
result
|
|
3494
|
+
]);
|
|
2763
3495
|
}
|
|
2764
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
3496
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "guard");
|
|
2765
3497
|
}
|
|
2766
3498
|
}
|
|
2767
3499
|
}
|
|
2768
3500
|
}
|
|
2769
3501
|
if ("redirect" in route2) {
|
|
2770
3502
|
const redirectPath = typeof route2.redirect === "function" ? route2.redirect(to) : route2.redirect;
|
|
2771
|
-
if (typeof redirectPath === "string" &&
|
|
2772
|
-
if (typeof console !== "undefined") {
|
|
3503
|
+
if (typeof redirectPath === "string" && !isRouterNavigable(redirectPath)) {
|
|
3504
|
+
if (classifyNavigationTarget(redirectPath) === "external" && typeof console !== "undefined") {
|
|
2773
3505
|
console.error(
|
|
2774
3506
|
`[SibuJS Router] Refusing absolute/protocol-relative redirect "${redirectPath}" \u2014 open-redirect risk.`
|
|
2775
3507
|
);
|
|
2776
3508
|
}
|
|
2777
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
2778
|
-
}
|
|
2779
|
-
if (typeof redirectPath === "string" && !isSafeNavigationTarget(redirectPath)) {
|
|
2780
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
3509
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
2781
3510
|
}
|
|
2782
|
-
return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1
|
|
3511
|
+
return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1, [
|
|
3512
|
+
...chain,
|
|
3513
|
+
redirectPath
|
|
3514
|
+
]);
|
|
2783
3515
|
}
|
|
2784
3516
|
}
|
|
2785
3517
|
const beforeResolveResult = await this.guards.runBeforeResolve(to, from, signal2);
|
|
2786
3518
|
if (beforeResolveResult !== true) {
|
|
2787
3519
|
if (typeof beforeResolveResult === "string") {
|
|
2788
|
-
if (!
|
|
2789
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
3520
|
+
if (!isRouterNavigable(beforeResolveResult)) {
|
|
3521
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
|
|
2790
3522
|
}
|
|
2791
|
-
return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1
|
|
3523
|
+
return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1, [
|
|
3524
|
+
...chain,
|
|
3525
|
+
beforeResolveResult
|
|
3526
|
+
]);
|
|
2792
3527
|
}
|
|
2793
|
-
throw new NavigationFailureError("aborted", from, to);
|
|
3528
|
+
throw new NavigationFailureError("aborted", from, to, void 0, "guard");
|
|
3529
|
+
}
|
|
3530
|
+
if (signal2.aborted) {
|
|
3531
|
+
throw new NavigationFailureError("aborted", from, to, void 0, abortReasonOf(signal2));
|
|
2794
3532
|
}
|
|
2795
3533
|
if (!options.skipHistory) {
|
|
2796
3534
|
this.updateHistory(to, options);
|
|
2797
3535
|
}
|
|
3536
|
+
if (!options.initialResolution) this.navEpoch++;
|
|
2798
3537
|
this.currentRouteSetter(to);
|
|
2799
3538
|
this.guards.runAfterEach(to, from);
|
|
2800
3539
|
this.handleScrollBehavior(to, from);
|
|
@@ -2822,29 +3561,76 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2822
3561
|
return path2;
|
|
2823
3562
|
}
|
|
2824
3563
|
isSameRoute(from, to) {
|
|
3564
|
+
if (from.matched.length === 0 && to.matched.length > 0) return false;
|
|
2825
3565
|
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;
|
|
2826
3566
|
}
|
|
2827
3567
|
updateHistory(to, options) {
|
|
2828
3568
|
const fullPath = this.options.base + to.path + (Object.keys(to.query).length ? `?${new URLSearchParams(to.query).toString()}` : "") + (to.hash ? `#${to.hash}` : "");
|
|
3569
|
+
const h = globalThis.history;
|
|
3570
|
+
if (!h) return;
|
|
2829
3571
|
if (options.replace) {
|
|
2830
|
-
|
|
3572
|
+
h.replaceState(options.state || {}, "", fullPath);
|
|
2831
3573
|
} else {
|
|
2832
|
-
|
|
3574
|
+
h.pushState(options.state || {}, "", fullPath);
|
|
2833
3575
|
}
|
|
2834
3576
|
}
|
|
3577
|
+
/**
|
|
3578
|
+
* Apply the configured `scrollBehavior`, if the runtime can actually scroll.
|
|
3579
|
+
*
|
|
3580
|
+
* This runs *after* the route has been committed, so an exception here does
|
|
3581
|
+
* not merely skip scrolling — it propagates out of `navigateInternal` and the
|
|
3582
|
+
* navigation is reported as `success: false` while the route state says it
|
|
3583
|
+
* succeeded. In a DOM-less runtime the bare `requestAnimationFrame` did
|
|
3584
|
+
* exactly that: a legal `scrollBehavior` option made every navigation report
|
|
3585
|
+
* failure under SSR and in memory-router-style tests. (MEM-001)
|
|
3586
|
+
*
|
|
3587
|
+
* Policy A, consistent with the history write in `updateHistory()` (NODE-001):
|
|
3588
|
+
* the route still commits, and only the browser-only side effect is skipped.
|
|
3589
|
+
*
|
|
3590
|
+
* Both primitives are probed, not just `window` — `requestAnimationFrame` and
|
|
3591
|
+
* `scrollTo` can be missing independently (jsdom without a rAF polyfill, a
|
|
3592
|
+
* partial DOM shim). `requestAnimationFrame` is invoked as a method on the
|
|
3593
|
+
* global object rather than through a detached reference, since browsers
|
|
3594
|
+
* reject a bare call with "Illegal invocation".
|
|
3595
|
+
*/
|
|
2835
3596
|
handleScrollBehavior(to, from) {
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
3597
|
+
const scrollBehavior = this.options.scrollBehavior;
|
|
3598
|
+
if (!scrollBehavior) return;
|
|
3599
|
+
const g2 = globalThis;
|
|
3600
|
+
if (typeof g2.requestAnimationFrame !== "function") return;
|
|
3601
|
+
if (typeof window === "undefined" || typeof window.scrollTo !== "function") return;
|
|
3602
|
+
let position;
|
|
3603
|
+
try {
|
|
3604
|
+
position = scrollBehavior(to, from, null);
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
if (typeof console !== "undefined") console.error("[router] scrollBehavior failed:", error);
|
|
3607
|
+
return;
|
|
2843
3608
|
}
|
|
3609
|
+
if (!position) return;
|
|
3610
|
+
const { x, y } = position;
|
|
3611
|
+
g2.requestAnimationFrame(() => {
|
|
3612
|
+
try {
|
|
3613
|
+
window.scrollTo(x, y);
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
if (typeof console !== "undefined") console.error("[router] scroll failed:", error);
|
|
3616
|
+
}
|
|
3617
|
+
});
|
|
2844
3618
|
}
|
|
2845
3619
|
// Component loading
|
|
2846
|
-
|
|
2847
|
-
|
|
3620
|
+
/**
|
|
3621
|
+
* Resolve how this route's component will be produced, without instantiating
|
|
3622
|
+
* anything. Pair with {@link instantiateComponent} at the commit boundary.
|
|
3623
|
+
*/
|
|
3624
|
+
async loadPlan(route2, routePath) {
|
|
3625
|
+
return this.loader.loadPlan(route2, routePath);
|
|
3626
|
+
}
|
|
3627
|
+
/** Create exactly one real instance from an already-resolved plan. */
|
|
3628
|
+
async instantiateComponent(plan, route2, routePath) {
|
|
3629
|
+
return this.loader.instantiate(plan, route2, routePath);
|
|
3630
|
+
}
|
|
3631
|
+
/** Resolve a route's module/factory without creating any component instance. */
|
|
3632
|
+
async preloadPlan(route2, routePath) {
|
|
3633
|
+
return this.loader.preloadPlan(route2, routePath);
|
|
2848
3634
|
}
|
|
2849
3635
|
// Guards API
|
|
2850
3636
|
beforeEach(guard) {
|
|
@@ -2879,12 +3665,18 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2879
3665
|
get isReady() {
|
|
2880
3666
|
return this.isReadyGetter();
|
|
2881
3667
|
}
|
|
3668
|
+
/** @internal Navigation generation — see `navEpoch`. Not a public API. */
|
|
3669
|
+
get navigationEpoch() {
|
|
3670
|
+
return this.navEpoch;
|
|
3671
|
+
}
|
|
2882
3672
|
get isNavigating() {
|
|
2883
3673
|
return this.navigator.isNavigating;
|
|
2884
3674
|
}
|
|
2885
3675
|
// Cleanup
|
|
2886
3676
|
destroy() {
|
|
2887
|
-
this.
|
|
3677
|
+
this.destroyed = true;
|
|
3678
|
+
this.navEpoch++;
|
|
3679
|
+
this.navigator.abort(ABORT_ROUTER_DESTROYED);
|
|
2888
3680
|
for (const fn of this.cleanup) fn();
|
|
2889
3681
|
this.cleanup = [];
|
|
2890
3682
|
this.guards.clear();
|
|
@@ -2927,10 +3719,11 @@ var _SibuRouter = class _SibuRouter {
|
|
|
2927
3719
|
_SibuRouter.MAX_REDIRECT_DEPTH = 10;
|
|
2928
3720
|
var SibuRouter = _SibuRouter;
|
|
2929
3721
|
var NavigationFailureError = class extends Error {
|
|
2930
|
-
constructor(type, from, to, error) {
|
|
2931
|
-
super(`Navigation ${type}: from ${from.path} to ${to.path}`);
|
|
3722
|
+
constructor(type, from, to, error, reason) {
|
|
3723
|
+
super(`Navigation ${type}${reason ? ` (${reason})` : ""}: from ${from.path} to ${to.path}`);
|
|
2932
3724
|
this.name = "NavigationFailureError";
|
|
2933
3725
|
this.type = type;
|
|
3726
|
+
this.reason = reason;
|
|
2934
3727
|
this.from = from;
|
|
2935
3728
|
this.to = to;
|
|
2936
3729
|
if (error) {
|
|
@@ -2940,12 +3733,16 @@ var NavigationFailureError = class extends Error {
|
|
|
2940
3733
|
toFailure() {
|
|
2941
3734
|
return {
|
|
2942
3735
|
type: this.type,
|
|
3736
|
+
reason: this.reason,
|
|
2943
3737
|
from: this.from,
|
|
2944
3738
|
to: this.to,
|
|
2945
3739
|
error: this.cause instanceof Error ? this.cause : void 0
|
|
2946
3740
|
};
|
|
2947
3741
|
}
|
|
2948
3742
|
};
|
|
3743
|
+
function abortReasonOf(signal2) {
|
|
3744
|
+
return signal2.reason === ABORT_ROUTER_DESTROYED ? "router-destroyed" : "superseded";
|
|
3745
|
+
}
|
|
2949
3746
|
var ROUTER_KEY = /* @__PURE__ */ Symbol.for("sibujs.router.v1");
|
|
2950
3747
|
var _routerRef = globalThis[ROUTER_KEY] ?? (globalThis[ROUTER_KEY] = { current: null });
|
|
2951
3748
|
function normalizeRoutes(routes) {
|
|
@@ -3049,6 +3846,13 @@ function Route() {
|
|
|
3049
3846
|
let errorNode = null;
|
|
3050
3847
|
let navSeq = 0;
|
|
3051
3848
|
let currentTopRoute = null;
|
|
3849
|
+
let routeTorn = false;
|
|
3850
|
+
const commitTarget = (seq) => routeTorn || seq !== navSeq ? null : anchor.parentNode;
|
|
3851
|
+
const release = (node) => {
|
|
3852
|
+
if (!node) return;
|
|
3853
|
+
dispose(node);
|
|
3854
|
+
node.parentNode?.removeChild(node);
|
|
3855
|
+
};
|
|
3052
3856
|
const cleanupNodes = () => {
|
|
3053
3857
|
[currentNode, loadingNode, errorNode].forEach((node) => {
|
|
3054
3858
|
if (!node) return;
|
|
@@ -3126,7 +3930,7 @@ function Route() {
|
|
|
3126
3930
|
anchor.parentNode.insertBefore(errorNode, anchor.nextSibling);
|
|
3127
3931
|
};
|
|
3128
3932
|
const update = async () => {
|
|
3129
|
-
if (!_routerRef.current) return;
|
|
3933
|
+
if (routeTorn || !_routerRef.current) return;
|
|
3130
3934
|
const seq = ++navSeq;
|
|
3131
3935
|
const route2 = _routerRef.current.currentRoute;
|
|
3132
3936
|
try {
|
|
@@ -3155,17 +3959,22 @@ function Route() {
|
|
|
3155
3959
|
if (isAsync) {
|
|
3156
3960
|
showLoading();
|
|
3157
3961
|
}
|
|
3158
|
-
const
|
|
3159
|
-
if (seq
|
|
3160
|
-
const node =
|
|
3161
|
-
|
|
3962
|
+
const plan = await _routerRef.current.loadPlan(routeDef, route2.path);
|
|
3963
|
+
if (!commitTarget(seq)) return;
|
|
3964
|
+
const node = await _routerRef.current.instantiateComponent(plan, routeDef, route2.path);
|
|
3965
|
+
const parent = commitTarget(seq);
|
|
3966
|
+
if (!parent) {
|
|
3967
|
+
release(node);
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
if (node) {
|
|
3162
3971
|
currentTopRoute = routeDef;
|
|
3163
3972
|
cleanupNodes();
|
|
3164
|
-
|
|
3973
|
+
parent.insertBefore(node, anchor.nextSibling);
|
|
3165
3974
|
currentNode = node;
|
|
3166
3975
|
}
|
|
3167
3976
|
} catch (error) {
|
|
3168
|
-
if (seq !== navSeq) return;
|
|
3977
|
+
if (routeTorn || seq !== navSeq) return;
|
|
3169
3978
|
hideLoading();
|
|
3170
3979
|
console.error("[Route] Component error:", error);
|
|
3171
3980
|
showError(error instanceof Error ? error : new Error(String(error)), routeDef);
|
|
@@ -3173,6 +3982,7 @@ function Route() {
|
|
|
3173
3982
|
}
|
|
3174
3983
|
} catch (error) {
|
|
3175
3984
|
if (seq !== navSeq) return;
|
|
3985
|
+
if (routeTorn) return;
|
|
3176
3986
|
console.error("[Route] Update failed:", error);
|
|
3177
3987
|
showError(error instanceof Error ? error : new Error(String(error)));
|
|
3178
3988
|
}
|
|
@@ -3189,12 +3999,13 @@ function Route() {
|
|
|
3189
3999
|
if (!routeInitialized && anchor.parentNode) wrappedUpdate();
|
|
3190
4000
|
});
|
|
3191
4001
|
}
|
|
3192
|
-
let routeTorn = false;
|
|
3193
4002
|
const routeCleanup = () => {
|
|
3194
4003
|
if (routeTorn) return;
|
|
3195
4004
|
routeTorn = true;
|
|
4005
|
+
navSeq++;
|
|
3196
4006
|
routeTeardown();
|
|
3197
4007
|
cleanupNodes();
|
|
4008
|
+
currentTopRoute = null;
|
|
3198
4009
|
};
|
|
3199
4010
|
registerDisposer(anchor, routeCleanup);
|
|
3200
4011
|
routeCleanups.push(routeCleanup);
|
|
@@ -3212,16 +4023,14 @@ function KeepAliveRoute(options) {
|
|
|
3212
4023
|
let currentNode = null;
|
|
3213
4024
|
let currentKey = "";
|
|
3214
4025
|
let currentCached = false;
|
|
3215
|
-
let
|
|
3216
|
-
let
|
|
4026
|
+
let updateSeq = 0;
|
|
4027
|
+
let kaTorn = false;
|
|
3217
4028
|
const update = async () => {
|
|
3218
|
-
if (!_routerRef.current) return;
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
const route2 = _routerRef.current.currentRoute;
|
|
3224
|
-
const match = _routerRef.current["matcher"].match(route2.path);
|
|
4029
|
+
if (kaTorn || !_routerRef.current) return;
|
|
4030
|
+
const seq = ++updateSeq;
|
|
4031
|
+
const router2 = _routerRef.current;
|
|
4032
|
+
const route2 = router2.currentRoute;
|
|
4033
|
+
const match = router2["matcher"].match(route2.path);
|
|
3225
4034
|
if (!match) return;
|
|
3226
4035
|
const { route: routeDef } = match;
|
|
3227
4036
|
if ("redirect" in routeDef) {
|
|
@@ -3238,63 +4047,62 @@ function KeepAliveRoute(options) {
|
|
|
3238
4047
|
const cacheKey = `${route2.path}${queryStr}${route2.hash ? `#${route2.hash}` : ""}`;
|
|
3239
4048
|
const shouldCache = !includeNames || routeDef.name != null && includeNames.includes(routeDef.name);
|
|
3240
4049
|
if (cacheKey === currentKey && currentNode) return;
|
|
3241
|
-
|
|
4050
|
+
if (!anchor.parentNode) return;
|
|
4051
|
+
let node = null;
|
|
4052
|
+
let fromCache = false;
|
|
4053
|
+
if (shouldCache && cache.has(cacheKey)) {
|
|
4054
|
+
node = cache.get(cacheKey);
|
|
4055
|
+
fromCache = true;
|
|
4056
|
+
} else {
|
|
4057
|
+
try {
|
|
4058
|
+
const plan = await router2.loadPlan(routeDef, route2.path);
|
|
4059
|
+
if (kaTorn || seq !== updateSeq) return;
|
|
4060
|
+
node = await router2.instantiateComponent(plan, routeDef, route2.path);
|
|
4061
|
+
} catch (error) {
|
|
4062
|
+
if (kaTorn || seq !== updateSeq) return;
|
|
4063
|
+
console.error("[KeepAliveRoute] Component error:", error);
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
if (!node) return;
|
|
4067
|
+
if (kaTorn || seq !== updateSeq) {
|
|
4068
|
+
dispose(node);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
3242
4072
|
const parent = anchor.parentNode;
|
|
3243
4073
|
if (!parent) {
|
|
3244
|
-
|
|
4074
|
+
if (!fromCache) dispose(node);
|
|
3245
4075
|
return;
|
|
3246
4076
|
}
|
|
3247
|
-
|
|
3248
|
-
if (currentNode
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
const component = await _routerRef.current.loadComponent(routeDef, route2.path);
|
|
3264
|
-
const node = component();
|
|
3265
|
-
if (!node || route2.path !== _routerRef.current.currentRoute.path) {
|
|
3266
|
-
isUpdating = false;
|
|
3267
|
-
return;
|
|
4077
|
+
if (currentNode && currentNode !== node) {
|
|
4078
|
+
if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
|
|
4079
|
+
if (!currentCached) dispose(currentNode);
|
|
4080
|
+
}
|
|
4081
|
+
if (fromCache) {
|
|
4082
|
+
const idx = lruOrder.indexOf(cacheKey);
|
|
4083
|
+
if (idx !== -1) lruOrder.splice(idx, 1);
|
|
4084
|
+
lruOrder.push(cacheKey);
|
|
4085
|
+
} else if (shouldCache) {
|
|
4086
|
+
cache.set(cacheKey, node);
|
|
4087
|
+
lruOrder.push(cacheKey);
|
|
4088
|
+
while (lruOrder.length > maxCache) {
|
|
4089
|
+
const evictKey = lruOrder.shift();
|
|
4090
|
+
if (evictKey === cacheKey) {
|
|
4091
|
+
lruOrder.push(evictKey);
|
|
4092
|
+
break;
|
|
3268
4093
|
}
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
while (lruOrder.length > maxCache) {
|
|
3275
|
-
const evictKey = lruOrder.shift();
|
|
3276
|
-
const evictNode = cache.get(evictKey);
|
|
3277
|
-
if (evictNode) {
|
|
3278
|
-
dispose(evictNode);
|
|
3279
|
-
if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
|
|
3280
|
-
cache.delete(evictKey);
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
4094
|
+
const evictNode = cache.get(evictKey);
|
|
4095
|
+
if (evictNode) {
|
|
4096
|
+
dispose(evictNode);
|
|
4097
|
+
if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
|
|
4098
|
+
cache.delete(evictKey);
|
|
3283
4099
|
}
|
|
3284
4100
|
}
|
|
3285
|
-
currentKey = cacheKey;
|
|
3286
|
-
if (currentNode) {
|
|
3287
|
-
parent.insertBefore(currentNode, anchor.nextSibling);
|
|
3288
|
-
}
|
|
3289
|
-
} catch (error) {
|
|
3290
|
-
console.error("[KeepAliveRoute] Component error:", error);
|
|
3291
|
-
} finally {
|
|
3292
|
-
isUpdating = false;
|
|
3293
|
-
if (pendingUpdate) {
|
|
3294
|
-
pendingUpdate = false;
|
|
3295
|
-
update();
|
|
3296
|
-
}
|
|
3297
4101
|
}
|
|
4102
|
+
currentNode = node;
|
|
4103
|
+
currentCached = fromCache || shouldCache;
|
|
4104
|
+
currentKey = cacheKey;
|
|
4105
|
+
parent.insertBefore(node, anchor.nextSibling);
|
|
3298
4106
|
};
|
|
3299
4107
|
let initialized = false;
|
|
3300
4108
|
const wrappedUpdate = async () => {
|
|
@@ -3307,10 +4115,10 @@ function KeepAliveRoute(options) {
|
|
|
3307
4115
|
if (!initialized && anchor.parentNode) wrappedUpdate();
|
|
3308
4116
|
});
|
|
3309
4117
|
}
|
|
3310
|
-
let kaTorn = false;
|
|
3311
4118
|
const kaCleanup = () => {
|
|
3312
4119
|
if (kaTorn) return;
|
|
3313
4120
|
kaTorn = true;
|
|
4121
|
+
updateSeq++;
|
|
3314
4122
|
kaTeardown();
|
|
3315
4123
|
for (const node of cache.values()) {
|
|
3316
4124
|
dispose(node);
|
|
@@ -3319,7 +4127,10 @@ function KeepAliveRoute(options) {
|
|
|
3319
4127
|
cache.clear();
|
|
3320
4128
|
lruOrder.length = 0;
|
|
3321
4129
|
if (currentNode?.parentNode) currentNode.parentNode.removeChild(currentNode);
|
|
4130
|
+
if (currentNode && !currentCached) dispose(currentNode);
|
|
3322
4131
|
currentNode = null;
|
|
4132
|
+
currentKey = "";
|
|
4133
|
+
currentCached = false;
|
|
3323
4134
|
};
|
|
3324
4135
|
registerDisposer(anchor, kaCleanup);
|
|
3325
4136
|
routeCleanups.push(kaCleanup);
|
|
@@ -3332,8 +4143,12 @@ function RouterLink(props, children) {
|
|
|
3332
4143
|
const baseClass = typeof classAttr === "string" ? classAttr : "";
|
|
3333
4144
|
const routeGetter = _routerRef.current.routeGetter;
|
|
3334
4145
|
const rawHref = _routerRef.current["resolvePath"](to);
|
|
3335
|
-
const
|
|
3336
|
-
const
|
|
4146
|
+
const kind = classifyNavigationTarget(rawHref);
|
|
4147
|
+
const href = kind === "unsafe" ? "#" : rawHref;
|
|
4148
|
+
const [hrefBeforeHash, hrefHash = ""] = href.split("#");
|
|
4149
|
+
const [hrefPathRaw, hrefQueryRaw = ""] = hrefBeforeHash.split("?");
|
|
4150
|
+
const targetPath = normalizePathname(hrefPathRaw);
|
|
4151
|
+
const targetQuery = normalizeQuery(hrefQueryRaw);
|
|
3337
4152
|
const link2 = document.createElement("a");
|
|
3338
4153
|
link2.href = href;
|
|
3339
4154
|
if (target) {
|
|
@@ -3349,8 +4164,9 @@ function RouterLink(props, children) {
|
|
|
3349
4164
|
const options = _routerRef.current["options"];
|
|
3350
4165
|
const effectCleanup = effect(() => {
|
|
3351
4166
|
const route2 = routeGetter();
|
|
3352
|
-
const
|
|
3353
|
-
const
|
|
4167
|
+
const currentPath = normalizePathname(route2.path);
|
|
4168
|
+
const isActive = kind === "internal" && isPathnameAncestor(targetPath, currentPath);
|
|
4169
|
+
const isExactActive = kind === "internal" && currentPath === targetPath && normalizeQuery(route2.query) === targetQuery && route2.hash === hrefHash;
|
|
3354
4170
|
const classes = [];
|
|
3355
4171
|
if (isActive) {
|
|
3356
4172
|
if (activeClass) classes.push(activeClass);
|
|
@@ -3372,7 +4188,7 @@ function RouterLink(props, children) {
|
|
|
3372
4188
|
const safe = sanitizeUrl(str);
|
|
3373
4189
|
if (safe) link2.setAttribute(key, safe);
|
|
3374
4190
|
} else if (lkey === "style") {
|
|
3375
|
-
link2.setAttribute(key,
|
|
4191
|
+
link2.setAttribute(key, sanitizeStyleAttribute(str));
|
|
3376
4192
|
} else {
|
|
3377
4193
|
link2.setAttribute(key, str);
|
|
3378
4194
|
}
|
|
@@ -3392,10 +4208,13 @@ function RouterLink(props, children) {
|
|
|
3392
4208
|
});
|
|
3393
4209
|
}
|
|
3394
4210
|
const onLinkClick = (e) => {
|
|
3395
|
-
if (target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
|
|
4211
|
+
if (e.defaultPrevented || target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
|
|
3396
4212
|
return;
|
|
3397
4213
|
}
|
|
4214
|
+
if (link2.hasAttribute("download")) return;
|
|
4215
|
+
if (kind === "external") return;
|
|
3398
4216
|
e.preventDefault();
|
|
4217
|
+
if (kind === "unsafe") return;
|
|
3399
4218
|
_routerRef.current?.navigate(to, { replace: replace2 }).catch((err) => {
|
|
3400
4219
|
if (typeof console !== "undefined") console.error("[router] link navigate failed:", err);
|
|
3401
4220
|
});
|
|
@@ -3411,68 +4230,80 @@ function Suspense(props) {
|
|
|
3411
4230
|
let currentNode = null;
|
|
3412
4231
|
let fallbackNode = null;
|
|
3413
4232
|
let isLoading = false;
|
|
4233
|
+
let generation = 0;
|
|
4234
|
+
let tornDown = false;
|
|
4235
|
+
const release = (node) => {
|
|
4236
|
+
if (!node) return;
|
|
4237
|
+
dispose(node);
|
|
4238
|
+
node.parentNode?.removeChild(node);
|
|
4239
|
+
};
|
|
4240
|
+
const commitTarget = (myGeneration) => tornDown || myGeneration !== generation ? null : anchor.parentNode;
|
|
3414
4241
|
const cleanupNodes = () => {
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
node.parentNode.removeChild(node);
|
|
3418
|
-
}
|
|
3419
|
-
});
|
|
4242
|
+
release(currentNode);
|
|
4243
|
+
release(fallbackNode);
|
|
3420
4244
|
currentNode = null;
|
|
3421
4245
|
fallbackNode = null;
|
|
3422
4246
|
};
|
|
3423
|
-
const showFallback = () => {
|
|
3424
|
-
if (fallbackNode || !props.fallback ||
|
|
4247
|
+
const showFallback = (myGeneration) => {
|
|
4248
|
+
if (fallbackNode || !props.fallback || tornDown) return;
|
|
4249
|
+
if (!anchor.parentNode) return;
|
|
4250
|
+
let fallback;
|
|
3425
4251
|
try {
|
|
3426
|
-
|
|
3427
|
-
if (fallback instanceof HTMLElement) {
|
|
3428
|
-
fallbackNode = fallback;
|
|
3429
|
-
anchor.parentNode.insertBefore(fallbackNode, anchor.nextSibling);
|
|
3430
|
-
}
|
|
4252
|
+
fallback = typeof props.fallback === "function" ? props.fallback() : props.fallback;
|
|
3431
4253
|
} catch (error) {
|
|
3432
4254
|
console.error("[Suspense] Fallback error:", error);
|
|
4255
|
+
return;
|
|
3433
4256
|
}
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
if (
|
|
3437
|
-
|
|
3438
|
-
|
|
4257
|
+
if (!(fallback instanceof HTMLElement)) return;
|
|
4258
|
+
const parent = commitTarget(myGeneration);
|
|
4259
|
+
if (!parent) {
|
|
4260
|
+
release(fallback);
|
|
4261
|
+
return;
|
|
3439
4262
|
}
|
|
4263
|
+
fallbackNode = fallback;
|
|
4264
|
+
parent.insertBefore(fallbackNode, anchor.nextSibling);
|
|
3440
4265
|
};
|
|
3441
4266
|
const render = async () => {
|
|
3442
|
-
if (isLoading) return;
|
|
4267
|
+
if (tornDown || isLoading) return;
|
|
3443
4268
|
isLoading = true;
|
|
4269
|
+
const myGeneration = ++generation;
|
|
3444
4270
|
try {
|
|
3445
4271
|
const result = props.nodes();
|
|
4272
|
+
let element;
|
|
3446
4273
|
if (result instanceof Promise) {
|
|
3447
|
-
showFallback();
|
|
3448
|
-
|
|
3449
|
-
if (anchor.parentNode) {
|
|
3450
|
-
cleanupNodes();
|
|
3451
|
-
anchor.parentNode.insertBefore(element, anchor.nextSibling);
|
|
3452
|
-
currentNode = element;
|
|
3453
|
-
}
|
|
4274
|
+
showFallback(myGeneration);
|
|
4275
|
+
element = await result;
|
|
3454
4276
|
} else {
|
|
3455
|
-
|
|
3456
|
-
cleanupNodes();
|
|
3457
|
-
anchor.parentNode.insertBefore(result, anchor.nextSibling);
|
|
3458
|
-
currentNode = result;
|
|
3459
|
-
}
|
|
4277
|
+
element = result;
|
|
3460
4278
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
if (anchor.parentNode) {
|
|
3465
|
-
const errorElement = document.createElement("div");
|
|
3466
|
-
errorElement.className = "suspense-error";
|
|
3467
|
-
errorElement.textContent = error instanceof Error ? error.message : "Failed to load";
|
|
4279
|
+
const parent = commitTarget(myGeneration);
|
|
4280
|
+
if (!parent) {
|
|
4281
|
+
release(element);
|
|
3468
4282
|
cleanupNodes();
|
|
3469
|
-
|
|
3470
|
-
currentNode = errorElement;
|
|
4283
|
+
return;
|
|
3471
4284
|
}
|
|
4285
|
+
cleanupNodes();
|
|
4286
|
+
parent.insertBefore(element, anchor.nextSibling);
|
|
4287
|
+
currentNode = element;
|
|
4288
|
+
} catch (error) {
|
|
4289
|
+
console.error("[Suspense] Nodes error:", error);
|
|
4290
|
+
const parent = commitTarget(myGeneration);
|
|
4291
|
+
cleanupNodes();
|
|
4292
|
+
if (!parent) return;
|
|
4293
|
+
const errorElement = document.createElement("div");
|
|
4294
|
+
errorElement.className = "suspense-error";
|
|
4295
|
+
errorElement.textContent = error instanceof Error ? error.message : "Failed to load";
|
|
4296
|
+
parent.insertBefore(errorElement, anchor.nextSibling);
|
|
4297
|
+
currentNode = errorElement;
|
|
3472
4298
|
} finally {
|
|
3473
4299
|
isLoading = false;
|
|
3474
4300
|
}
|
|
3475
4301
|
};
|
|
4302
|
+
registerDisposer(anchor, () => {
|
|
4303
|
+
tornDown = true;
|
|
4304
|
+
generation++;
|
|
4305
|
+
cleanupNodes();
|
|
4306
|
+
});
|
|
3476
4307
|
queueMicrotask(render);
|
|
3477
4308
|
return anchor;
|
|
3478
4309
|
}
|
|
@@ -3487,12 +4318,15 @@ async function preloadRoute(to) {
|
|
|
3487
4318
|
const match = _routerRef.current["matcher"].match(path2.split("?")[0].split("#")[0]);
|
|
3488
4319
|
if (match && "component" in match.route) {
|
|
3489
4320
|
try {
|
|
3490
|
-
await _routerRef.current.
|
|
4321
|
+
await _routerRef.current.preloadPlan(match.route, path2);
|
|
3491
4322
|
} catch (error) {
|
|
3492
4323
|
console.warn("[Router] Preload failed:", error);
|
|
3493
4324
|
}
|
|
3494
4325
|
}
|
|
3495
4326
|
}
|
|
4327
|
+
function __getNavigationEpoch() {
|
|
4328
|
+
return _routerRef.current?.navigationEpoch ?? -1;
|
|
4329
|
+
}
|
|
3496
4330
|
function hasRoute(name) {
|
|
3497
4331
|
if (!_routerRef.current) return false;
|
|
3498
4332
|
return _routerRef.current["matcher"].findByName(name) !== null;
|
|
@@ -3534,6 +4368,7 @@ function Outlet() {
|
|
|
3534
4368
|
let currentNode = null;
|
|
3535
4369
|
let currentChild = null;
|
|
3536
4370
|
let navSeq = 0;
|
|
4371
|
+
let outletTorn = false;
|
|
3537
4372
|
const clearCurrent = () => {
|
|
3538
4373
|
if (currentNode) {
|
|
3539
4374
|
dispose(currentNode);
|
|
@@ -3542,8 +4377,14 @@ function Outlet() {
|
|
|
3542
4377
|
}
|
|
3543
4378
|
currentChild = null;
|
|
3544
4379
|
};
|
|
4380
|
+
const commitTarget = (seq) => outletTorn || seq !== navSeq ? null : anchor.parentNode;
|
|
4381
|
+
const release = (node) => {
|
|
4382
|
+
if (!node) return;
|
|
4383
|
+
dispose(node);
|
|
4384
|
+
node.parentNode?.removeChild(node);
|
|
4385
|
+
};
|
|
3545
4386
|
const update = async () => {
|
|
3546
|
-
if (!_routerRef.current) return;
|
|
4387
|
+
if (outletTorn || !_routerRef.current) return;
|
|
3547
4388
|
const seq = ++navSeq;
|
|
3548
4389
|
const route2 = _routerRef.current.currentRoute;
|
|
3549
4390
|
if (route2.matched.length < 2) {
|
|
@@ -3558,17 +4399,21 @@ function Outlet() {
|
|
|
3558
4399
|
if (childRoute === currentChild && currentNode) return;
|
|
3559
4400
|
try {
|
|
3560
4401
|
const cacheKey = `${route2.path}\0${childRoute.path}`;
|
|
3561
|
-
const
|
|
3562
|
-
if (seq
|
|
3563
|
-
const node =
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
currentChild = childRoute;
|
|
4402
|
+
const plan = await _routerRef.current.loadPlan(childRoute, cacheKey);
|
|
4403
|
+
if (!commitTarget(seq)) return;
|
|
4404
|
+
const node = await _routerRef.current.instantiateComponent(plan, childRoute, cacheKey);
|
|
4405
|
+
const parent = commitTarget(seq);
|
|
4406
|
+
if (!parent) {
|
|
4407
|
+
release(node);
|
|
4408
|
+
return;
|
|
3569
4409
|
}
|
|
4410
|
+
if (!node) return;
|
|
4411
|
+
clearCurrent();
|
|
4412
|
+
parent.insertBefore(node, anchor.nextSibling);
|
|
4413
|
+
currentNode = node;
|
|
4414
|
+
currentChild = childRoute;
|
|
3570
4415
|
} catch (error) {
|
|
3571
|
-
if (seq !== navSeq) return;
|
|
4416
|
+
if (outletTorn || seq !== navSeq) return;
|
|
3572
4417
|
console.error("[Outlet] Failed to render child route:", error);
|
|
3573
4418
|
}
|
|
3574
4419
|
};
|
|
@@ -3578,16 +4423,17 @@ function Outlet() {
|
|
|
3578
4423
|
if (anchor.parentNode) update();
|
|
3579
4424
|
});
|
|
3580
4425
|
}
|
|
3581
|
-
let outletTorn = false;
|
|
3582
4426
|
const outletCleanup = () => {
|
|
3583
4427
|
if (outletTorn) return;
|
|
3584
4428
|
outletTorn = true;
|
|
4429
|
+
navSeq++;
|
|
3585
4430
|
outletTeardown();
|
|
3586
4431
|
if (currentNode) {
|
|
3587
4432
|
dispose(currentNode);
|
|
3588
4433
|
if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
|
|
3589
4434
|
currentNode = null;
|
|
3590
4435
|
}
|
|
4436
|
+
currentChild = null;
|
|
3591
4437
|
};
|
|
3592
4438
|
registerDisposer(anchor, outletCleanup);
|
|
3593
4439
|
routeCleanups.push(outletCleanup);
|
|
@@ -3658,6 +4504,8 @@ function isUnsafeKey(key) {
|
|
|
3658
4504
|
}
|
|
3659
4505
|
|
|
3660
4506
|
// src/plugins/routerSSR.ts
|
|
4507
|
+
init_headEntry();
|
|
4508
|
+
init_sanitize();
|
|
3661
4509
|
function safeDecode(raw) {
|
|
3662
4510
|
try {
|
|
3663
4511
|
return decodeURIComponent(raw);
|
|
@@ -3855,22 +4703,16 @@ function renderRouteToString(url, routes, _options) {
|
|
|
3855
4703
|
};
|
|
3856
4704
|
}
|
|
3857
4705
|
function renderRouteToDocument(url, routes, options) {
|
|
3858
|
-
const { html: html2, state } = renderRouteToString(url, routes, options);
|
|
4706
|
+
const { html: html2, state: state2 } = renderRouteToString(url, routes, options);
|
|
3859
4707
|
const opts = options || {};
|
|
3860
|
-
const metaTags = (opts.meta || []).
|
|
3861
|
-
|
|
3862
|
-
return pairs ? `<meta ${pairs} />` : "";
|
|
3863
|
-
}).filter(Boolean).join("\n ");
|
|
3864
|
-
const linkTags = (opts.links || []).map((attrs) => {
|
|
3865
|
-
const pairs = buildSafeAttrString(attrs);
|
|
3866
|
-
return pairs ? `<link ${pairs} />` : "";
|
|
3867
|
-
}).filter(Boolean).join("\n ");
|
|
4708
|
+
const metaTags = (opts.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
|
|
4709
|
+
const linkTags = (opts.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
|
|
3868
4710
|
const scriptTags = (opts.scripts || []).map((src) => {
|
|
3869
|
-
const safe =
|
|
4711
|
+
const safe = sanitizeUrl(String(src));
|
|
3870
4712
|
if (!safe) return "";
|
|
3871
4713
|
return `<script src="${escapeAttrLocal(safe)}"></script>`;
|
|
3872
4714
|
}).filter(Boolean).join("\n ");
|
|
3873
|
-
const stateScript = serializeRouteState(
|
|
4715
|
+
const stateScript = serializeRouteState(state2, opts.nonce);
|
|
3874
4716
|
return `<!DOCTYPE html>
|
|
3875
4717
|
<html>
|
|
3876
4718
|
<head>
|
|
@@ -3888,8 +4730,8 @@ function renderRouteToDocument(url, routes, options) {
|
|
|
3888
4730
|
</body>
|
|
3889
4731
|
</html>`;
|
|
3890
4732
|
}
|
|
3891
|
-
function serializeRouteState(
|
|
3892
|
-
const json = escapeScriptJson(JSON.stringify(
|
|
4733
|
+
function serializeRouteState(state2, nonce) {
|
|
4734
|
+
const json = escapeScriptJson(JSON.stringify(state2));
|
|
3893
4735
|
const nonceAttr = nonce ? ` nonce="${escapeAttrLocal(nonce)}"` : "";
|
|
3894
4736
|
return `<script${nonceAttr}>window.${SSR_ROUTE_STATE_KEY}=${json}</script>`;
|
|
3895
4737
|
}
|
|
@@ -3905,20 +4747,23 @@ function hydrateRouter(routes, options) {
|
|
|
3905
4747
|
}
|
|
3906
4748
|
createRouter(routes);
|
|
3907
4749
|
const container = options?.container || document.getElementById("app");
|
|
3908
|
-
if (container
|
|
3909
|
-
|
|
4750
|
+
if (!container) return;
|
|
4751
|
+
const liveUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
4752
|
+
const resolved = resolveServerRoute(liveUrl, routes);
|
|
4753
|
+
const bootstrapEpoch = __getNavigationEpoch();
|
|
4754
|
+
Promise.resolve().then(() => (init_ssr(), ssr_exports)).then(async ({ hydrate: hydrate2 }) => {
|
|
4755
|
+
const { replaceChildrenSafely: replaceChildrenSafely2 } = await Promise.resolve().then(() => (init_dispose(), dispose_exports));
|
|
4756
|
+
if (__getNavigationEpoch() !== bootstrapEpoch) return;
|
|
3910
4757
|
if (resolved.component) {
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
hydrate2(resolved.component, container);
|
|
3914
|
-
}
|
|
3915
|
-
}).catch((err) => {
|
|
3916
|
-
if (typeof console !== "undefined") {
|
|
3917
|
-
console.error("[SibuJS routerSSR] failed to load hydrate:", err);
|
|
3918
|
-
}
|
|
3919
|
-
});
|
|
4758
|
+
hydrate2(resolved.component, container);
|
|
4759
|
+
return;
|
|
3920
4760
|
}
|
|
3921
|
-
|
|
4761
|
+
replaceChildrenSafely2(container);
|
|
4762
|
+
}).catch((err) => {
|
|
4763
|
+
if (typeof console !== "undefined") {
|
|
4764
|
+
console.error("[SibuJS routerSSR] failed to load hydrate:", err);
|
|
4765
|
+
}
|
|
4766
|
+
});
|
|
3922
4767
|
}
|
|
3923
4768
|
function createSSRRouter(routes) {
|
|
3924
4769
|
return {
|
|
@@ -3933,54 +4778,6 @@ function createSSRRouter(routes) {
|
|
|
3933
4778
|
}
|
|
3934
4779
|
};
|
|
3935
4780
|
}
|
|
3936
|
-
var SAFE_ATTR_NAME2 = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
|
|
3937
|
-
function isSafeAttrName2(name) {
|
|
3938
|
-
return SAFE_ATTR_NAME2.test(name);
|
|
3939
|
-
}
|
|
3940
|
-
function isEventHandlerAttr2(name) {
|
|
3941
|
-
if (name.length < 3) return false;
|
|
3942
|
-
const lower = name.toLowerCase();
|
|
3943
|
-
return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
|
|
3944
|
-
}
|
|
3945
|
-
var URL_ATTRS2 = /* @__PURE__ */ new Set([
|
|
3946
|
-
"href",
|
|
3947
|
-
"src",
|
|
3948
|
-
"action",
|
|
3949
|
-
"formaction",
|
|
3950
|
-
"cite",
|
|
3951
|
-
"poster",
|
|
3952
|
-
"background",
|
|
3953
|
-
"srcset",
|
|
3954
|
-
"ping",
|
|
3955
|
-
"manifest",
|
|
3956
|
-
"data",
|
|
3957
|
-
"xlink:href"
|
|
3958
|
-
]);
|
|
3959
|
-
function sanitizeUrlLocal(url) {
|
|
3960
|
-
const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
|
|
3961
|
-
if (!trimmed) return "";
|
|
3962
|
-
const lower = trimmed.toLowerCase();
|
|
3963
|
-
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
|
|
3964
|
-
return "";
|
|
3965
|
-
}
|
|
3966
|
-
return trimmed;
|
|
3967
|
-
}
|
|
3968
|
-
function buildSafeAttrString(attrs) {
|
|
3969
|
-
const out = [];
|
|
3970
|
-
for (const rawKey of Object.keys(attrs)) {
|
|
3971
|
-
if (!Object.hasOwn(attrs, rawKey)) continue;
|
|
3972
|
-
if (!isSafeAttrName2(rawKey)) continue;
|
|
3973
|
-
if (isEventHandlerAttr2(rawKey)) continue;
|
|
3974
|
-
const lowerKey = rawKey.toLowerCase();
|
|
3975
|
-
let value = String(attrs[rawKey]);
|
|
3976
|
-
if (URL_ATTRS2.has(lowerKey)) {
|
|
3977
|
-
value = sanitizeUrlLocal(value);
|
|
3978
|
-
if (!value) continue;
|
|
3979
|
-
}
|
|
3980
|
-
out.push(`${rawKey}="${escapeAttrLocal(value)}"`);
|
|
3981
|
-
}
|
|
3982
|
-
return out.join(" ");
|
|
3983
|
-
}
|
|
3984
4781
|
function escapeHtmlLocal(str) {
|
|
3985
4782
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3986
4783
|
}
|
|
@@ -4813,6 +5610,7 @@ function createBootSequence() {
|
|
|
4813
5610
|
Suspense,
|
|
4814
5611
|
Trans,
|
|
4815
5612
|
VERSION,
|
|
5613
|
+
__getNavigationEpoch,
|
|
4816
5614
|
__removeRouterPagehideHandler,
|
|
4817
5615
|
addRoute,
|
|
4818
5616
|
afterEach,
|