sibujs 1.1.0 → 1.3.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/README.md +29 -25
- package/dist/browser.cjs +804 -2
- package/dist/browser.d.cts +591 -1
- package/dist/browser.d.ts +591 -1
- package/dist/browser.js +50 -8
- package/dist/build.cjs +706 -161
- package/dist/build.js +21 -12
- package/dist/cdn.global.js +188 -7
- package/dist/chunk-2BYQDGN3.js +742 -0
- package/dist/chunk-32DY64NT.js +282 -0
- package/dist/chunk-3AIRKM3B.js +1263 -0
- package/dist/chunk-3X2YG6YM.js +505 -0
- package/dist/chunk-5X6PP2UK.js +28 -0
- package/dist/chunk-77L6NL3X.js +1097 -0
- package/dist/chunk-B7SWRFUT.js +332 -0
- package/dist/chunk-BGN5ZMP4.js +26 -0
- package/dist/chunk-BTU3TJDS.js +365 -0
- package/dist/chunk-CHF5OHIA.js +61 -0
- package/dist/chunk-CMBFNA7L.js +27 -0
- package/dist/chunk-DAHRH4ON.js +331 -0
- package/dist/chunk-EBGIRKQY.js +616 -0
- package/dist/chunk-EUZND3CB.js +27 -0
- package/dist/chunk-F3FA4F32.js +292 -0
- package/dist/chunk-GCOK2LC3.js +282 -0
- package/dist/chunk-JAKHTMQU.js +1000 -0
- package/dist/chunk-JCI5M6U6.js +956 -0
- package/dist/chunk-KQPDEVVS.js +398 -0
- package/dist/chunk-NEKUBFPT.js +60 -0
- package/dist/chunk-NYVAC6P5.js +37 -0
- package/dist/chunk-OUZZEE4S.js +365 -0
- package/dist/chunk-P6W3STU4.js +2249 -0
- package/dist/chunk-PTQJDMRT.js +146 -0
- package/dist/chunk-QWZG56ET.js +2744 -0
- package/dist/chunk-TSOKIX5Z.js +654 -0
- package/dist/chunk-VMVDTCXB.js +712 -0
- package/dist/chunk-VRW3FULF.js +725 -0
- package/dist/chunk-WZSPOOER.js +84 -0
- package/dist/chunk-YT6HQ6AM.js +14 -0
- package/dist/chunk-ZD6OAMTH.js +277 -0
- package/dist/contracts-DDrwxvJ-.d.cts +245 -0
- package/dist/contracts-DDrwxvJ-.d.ts +245 -0
- package/dist/data.cjs +35 -2
- package/dist/data.d.cts +7 -0
- package/dist/data.d.ts +7 -0
- package/dist/data.js +9 -8
- package/dist/devtools.cjs +122 -0
- package/dist/devtools.d.cts +69 -461
- package/dist/devtools.d.ts +69 -461
- package/dist/devtools.js +127 -6
- package/dist/ecosystem.cjs +68 -23
- package/dist/ecosystem.d.cts +1 -1
- package/dist/ecosystem.d.ts +1 -1
- package/dist/ecosystem.js +10 -9
- package/dist/extras.cjs +1252 -82
- package/dist/extras.d.cts +5 -5
- package/dist/extras.d.ts +5 -5
- package/dist/extras.js +69 -24
- package/dist/index.cjs +708 -161
- package/dist/index.d.cts +397 -17
- package/dist/index.d.ts +397 -17
- package/dist/index.js +39 -17
- package/dist/introspect-BumjnBKr.d.cts +477 -0
- package/dist/introspect-CZrlcaYy.d.ts +477 -0
- package/dist/introspect-Cb0zgpi2.d.cts +477 -0
- package/dist/introspect-Y2xNXGSf.d.ts +477 -0
- package/dist/motion.js +4 -4
- package/dist/patterns.cjs +51 -2
- package/dist/patterns.d.cts +18 -8
- package/dist/patterns.d.ts +18 -8
- package/dist/patterns.js +7 -7
- package/dist/performance.js +4 -4
- package/dist/plugins.cjs +473 -98
- package/dist/plugins.d.cts +27 -4
- package/dist/plugins.d.ts +27 -4
- package/dist/plugins.js +156 -37
- package/dist/ssr-4PBXAOO3.js +40 -0
- package/dist/ssr-Do_SiVoL.d.cts +201 -0
- package/dist/ssr-Do_SiVoL.d.ts +201 -0
- package/dist/ssr.cjs +357 -77
- package/dist/ssr.d.cts +10 -1
- package/dist/ssr.d.ts +10 -1
- package/dist/ssr.js +13 -10
- package/dist/tagFactory-DaJ0YWX6.d.cts +47 -0
- package/dist/tagFactory-DaJ0YWX6.d.ts +47 -0
- package/dist/testing.cjs +233 -2
- package/dist/testing.d.cts +42 -1
- package/dist/testing.d.ts +42 -1
- package/dist/testing.js +129 -2
- package/dist/ui.cjs +374 -3
- package/dist/ui.d.cts +252 -2
- package/dist/ui.d.ts +252 -2
- package/dist/ui.js +328 -8
- package/dist/widgets.js +7 -7
- package/package.json +1 -1
package/dist/plugins.cjs
CHANGED
|
@@ -42,11 +42,40 @@ var init_dev = __esm({
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
// src/utils/sanitize.ts
|
|
46
|
+
function sanitizeUrl(url) {
|
|
47
|
+
const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
|
|
48
|
+
if (!trimmed) return "";
|
|
49
|
+
const lower = trimmed.toLowerCase();
|
|
50
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
return trimmed;
|
|
54
|
+
}
|
|
55
|
+
function sanitizeCSSValue(value) {
|
|
56
|
+
const lower = value.toLowerCase().replace(/\s+/g, "");
|
|
57
|
+
if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("-moz-binding")) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function isUrlAttribute(attr) {
|
|
63
|
+
return URL_ATTRIBUTES.has(attr);
|
|
64
|
+
}
|
|
65
|
+
var URL_ATTRIBUTES;
|
|
66
|
+
var init_sanitize = __esm({
|
|
67
|
+
"src/utils/sanitize.ts"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "cite", "poster", "background", "srcset"]);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
45
73
|
// src/platform/ssr.ts
|
|
46
74
|
var ssr_exports = {};
|
|
47
75
|
__export(ssr_exports, {
|
|
48
76
|
collectStream: () => collectStream,
|
|
49
77
|
deserializeState: () => deserializeState,
|
|
78
|
+
escapeScriptJson: () => escapeScriptJson,
|
|
50
79
|
hydrate: () => hydrate,
|
|
51
80
|
hydrateIslands: () => hydrateIslands,
|
|
52
81
|
hydrateProgressively: () => hydrateProgressively,
|
|
@@ -62,12 +91,24 @@ __export(ssr_exports, {
|
|
|
62
91
|
suspenseSwapScript: () => suspenseSwapScript,
|
|
63
92
|
trustHTML: () => trustHTML
|
|
64
93
|
});
|
|
94
|
+
function isSafeAttrName(name) {
|
|
95
|
+
return SAFE_ATTR_NAME.test(name);
|
|
96
|
+
}
|
|
97
|
+
function isEventHandlerAttr2(name) {
|
|
98
|
+
if (name.length < 3) return false;
|
|
99
|
+
const lower = name.toLowerCase();
|
|
100
|
+
return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
|
|
101
|
+
}
|
|
65
102
|
function ssrErrorComment(err) {
|
|
66
103
|
if (_isDev7) {
|
|
67
|
-
|
|
104
|
+
const msg = escapeHtml(err instanceof Error ? err.message : String(err));
|
|
105
|
+
return `<!--SSR error: ${safeCommentText(msg)}-->`;
|
|
68
106
|
}
|
|
69
107
|
return "<!--SSR error-->";
|
|
70
108
|
}
|
|
109
|
+
function safeCommentText(text2) {
|
|
110
|
+
return text2.replace(/-->/g, "-->").replace(/--!>/g, "--!>").replace(/<!--/g, "<!--").replace(/--$/g, "---");
|
|
111
|
+
}
|
|
71
112
|
function renderToString(element) {
|
|
72
113
|
if (element instanceof DocumentFragment) {
|
|
73
114
|
return Array.from(element.childNodes).map((child) => {
|
|
@@ -82,16 +123,30 @@ function renderToString(element) {
|
|
|
82
123
|
return escapeHtml(element.textContent || "");
|
|
83
124
|
}
|
|
84
125
|
if (element.nodeType === 8) {
|
|
85
|
-
|
|
86
|
-
return `<!--${content}-->`;
|
|
126
|
+
return `<!--${safeCommentText(element.textContent || "")}-->`;
|
|
87
127
|
}
|
|
88
128
|
if (!(element instanceof HTMLElement)) {
|
|
89
|
-
return element.textContent || "";
|
|
129
|
+
return escapeHtml(element.textContent || "");
|
|
90
130
|
}
|
|
91
131
|
const tag = element.tagName.toLowerCase();
|
|
132
|
+
if (tag === "script" || tag === "style") {
|
|
133
|
+
return _isDev7 ? `<!--ssr:${tag}-stripped-->` : "";
|
|
134
|
+
}
|
|
135
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
|
|
136
|
+
return _isDev7 ? "<!--ssr:invalid-tag-->" : "";
|
|
137
|
+
}
|
|
92
138
|
let html2 = `<${tag}`;
|
|
93
139
|
for (const attr of Array.from(element.attributes)) {
|
|
94
|
-
|
|
140
|
+
const rawName = attr.name;
|
|
141
|
+
if (!isSafeAttrName(rawName)) continue;
|
|
142
|
+
if (isEventHandlerAttr2(rawName)) continue;
|
|
143
|
+
const lowerName = rawName.toLowerCase();
|
|
144
|
+
let value = attr.value;
|
|
145
|
+
if (URL_ATTRS.has(lowerName)) {
|
|
146
|
+
value = sanitizeUrl(value);
|
|
147
|
+
if (!value) continue;
|
|
148
|
+
}
|
|
149
|
+
html2 += ` ${rawName}="${escapeAttr(value)}"`;
|
|
95
150
|
}
|
|
96
151
|
if (element.dataset && !element.dataset.sibuHydrate) {
|
|
97
152
|
html2 += ` data-sibu-ssr="true"`;
|
|
@@ -110,8 +165,25 @@ function renderToString(element) {
|
|
|
110
165
|
html2 += `</${tag}>`;
|
|
111
166
|
return html2;
|
|
112
167
|
}
|
|
113
|
-
function hydrate(component, container) {
|
|
168
|
+
function hydrate(component, container, options = {}) {
|
|
114
169
|
const clientTree = component();
|
|
170
|
+
if (options.diagnostics) {
|
|
171
|
+
const mismatches = [];
|
|
172
|
+
collectMismatches(container.firstElementChild, clientTree, "", mismatches);
|
|
173
|
+
if (mismatches.length > 0) {
|
|
174
|
+
const first = mismatches[0];
|
|
175
|
+
if (options.onMismatch) {
|
|
176
|
+
options.onMismatch(first);
|
|
177
|
+
} else if (_isDev7) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`[Sibu hydration] ${first.message}
|
|
180
|
+
at ${first.path}
|
|
181
|
+
server: ${first.serverValue}
|
|
182
|
+
client: ${first.clientValue}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
115
187
|
hydrateNode(container.firstElementChild, clientTree);
|
|
116
188
|
container.setAttribute("data-sibu-hydrated", "true");
|
|
117
189
|
}
|
|
@@ -123,9 +195,119 @@ function hydrateNode(serverNode, clientNode) {
|
|
|
123
195
|
hydrateNode(serverChildren[i2], clientChildren[i2]);
|
|
124
196
|
}
|
|
125
197
|
}
|
|
198
|
+
function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
|
|
199
|
+
if (out.length >= max) return;
|
|
200
|
+
const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
|
|
201
|
+
if (!serverNode && clientNode) {
|
|
202
|
+
out.push({
|
|
203
|
+
kind: "child-count",
|
|
204
|
+
path: nodePath,
|
|
205
|
+
serverValue: "(missing)",
|
|
206
|
+
clientValue: clientNode.tagName.toLowerCase(),
|
|
207
|
+
message: "Client rendered a node that the server did not emit."
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (serverNode && !clientNode) {
|
|
212
|
+
out.push({
|
|
213
|
+
kind: "child-count",
|
|
214
|
+
path: nodePath,
|
|
215
|
+
serverValue: serverNode.tagName.toLowerCase(),
|
|
216
|
+
clientValue: "(missing)",
|
|
217
|
+
message: "Server rendered a node that the client did not produce."
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!serverNode || !clientNode) return;
|
|
222
|
+
if (serverNode.tagName !== clientNode.tagName) {
|
|
223
|
+
out.push({
|
|
224
|
+
kind: "tag",
|
|
225
|
+
path: nodePath,
|
|
226
|
+
serverValue: serverNode.tagName.toLowerCase(),
|
|
227
|
+
clientValue: clientNode.tagName.toLowerCase(),
|
|
228
|
+
message: "Element tag mismatch \u2014 server and client disagree on the element type."
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const skipAttrs = /* @__PURE__ */ new Set(["data-sibu-ssr", "data-sibu-hydrated", "data-sibu-island"]);
|
|
233
|
+
const serverAttrs = /* @__PURE__ */ new Map();
|
|
234
|
+
for (const a2 of Array.from(serverNode.attributes)) {
|
|
235
|
+
if (!skipAttrs.has(a2.name)) serverAttrs.set(a2.name, a2.value);
|
|
236
|
+
}
|
|
237
|
+
const clientAttrs = /* @__PURE__ */ new Map();
|
|
238
|
+
for (const a2 of Array.from(clientNode.attributes)) {
|
|
239
|
+
if (!skipAttrs.has(a2.name)) clientAttrs.set(a2.name, a2.value);
|
|
240
|
+
}
|
|
241
|
+
for (const [name, value] of serverAttrs) {
|
|
242
|
+
if (out.length >= max) return;
|
|
243
|
+
if (!clientAttrs.has(name)) {
|
|
244
|
+
out.push({
|
|
245
|
+
kind: "attribute",
|
|
246
|
+
path: `${nodePath}[${name}]`,
|
|
247
|
+
serverValue: value,
|
|
248
|
+
clientValue: "(missing)",
|
|
249
|
+
message: `Attribute "${name}" present on server but missing on client.`
|
|
250
|
+
});
|
|
251
|
+
} else if (clientAttrs.get(name) !== value) {
|
|
252
|
+
out.push({
|
|
253
|
+
kind: "attribute",
|
|
254
|
+
path: `${nodePath}[${name}]`,
|
|
255
|
+
serverValue: value,
|
|
256
|
+
clientValue: clientAttrs.get(name) ?? "",
|
|
257
|
+
message: `Attribute "${name}" differs between server and client.`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const [name, value] of clientAttrs) {
|
|
262
|
+
if (out.length >= max) return;
|
|
263
|
+
if (!serverAttrs.has(name)) {
|
|
264
|
+
out.push({
|
|
265
|
+
kind: "attribute",
|
|
266
|
+
path: `${nodePath}[${name}]`,
|
|
267
|
+
serverValue: "(missing)",
|
|
268
|
+
clientValue: value,
|
|
269
|
+
message: `Attribute "${name}" present on client but missing on server.`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const serverChildren = Array.from(serverNode.children);
|
|
274
|
+
const clientChildren = Array.from(clientNode.children);
|
|
275
|
+
const max2 = Math.max(serverChildren.length, clientChildren.length);
|
|
276
|
+
for (let i2 = 0; i2 < max2; i2++) {
|
|
277
|
+
if (out.length >= max) return;
|
|
278
|
+
const childPath = `${nodePath} > ${clientChildren[i2]?.tagName?.toLowerCase() ?? serverChildren[i2]?.tagName?.toLowerCase() ?? "?"}:nth-child(${i2 + 1})`;
|
|
279
|
+
collectMismatches(serverChildren[i2] ?? null, clientChildren[i2] ?? null, childPath, out, max);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
126
282
|
function trustHTML(html2) {
|
|
127
283
|
return html2;
|
|
128
284
|
}
|
|
285
|
+
function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
|
|
286
|
+
if (!attrs) return "";
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const rawKey of Object.keys(attrs)) {
|
|
289
|
+
if (!Object.hasOwn(attrs, rawKey)) continue;
|
|
290
|
+
if (!isSafeAttrName(rawKey)) continue;
|
|
291
|
+
if (!allowEventHandlers && isEventHandlerAttr2(rawKey)) continue;
|
|
292
|
+
const lowerKey = rawKey.toLowerCase();
|
|
293
|
+
let value = String(attrs[rawKey]);
|
|
294
|
+
if (URL_ATTRS.has(lowerKey)) {
|
|
295
|
+
value = sanitizeUrl(value);
|
|
296
|
+
if (!value) continue;
|
|
297
|
+
}
|
|
298
|
+
out.push(`${rawKey}="${escapeAttr(value)}"`);
|
|
299
|
+
}
|
|
300
|
+
return out.join(" ");
|
|
301
|
+
}
|
|
302
|
+
function isDangerousMetaRefresh(metaProps) {
|
|
303
|
+
const httpEquiv = metaProps["http-equiv"];
|
|
304
|
+
if (typeof httpEquiv !== "string") return false;
|
|
305
|
+
if (httpEquiv.toLowerCase() !== "refresh") return false;
|
|
306
|
+
const content = metaProps.content;
|
|
307
|
+
if (typeof content !== "string") return false;
|
|
308
|
+
const normalized = content.replace(/[\x00-\x20\x7f-\x9f]+/g, "").toLowerCase();
|
|
309
|
+
return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
|
|
310
|
+
}
|
|
129
311
|
function renderToDocument(component, options = {}) {
|
|
130
312
|
let content;
|
|
131
313
|
try {
|
|
@@ -133,14 +315,22 @@ function renderToDocument(component, options = {}) {
|
|
|
133
315
|
} catch (err) {
|
|
134
316
|
content = ssrErrorComment(err);
|
|
135
317
|
}
|
|
136
|
-
const metaTags = (options.meta || []).map(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
).
|
|
142
|
-
|
|
143
|
-
|
|
318
|
+
const metaTags = (options.meta || []).map((attrs) => {
|
|
319
|
+
if (isDangerousMetaRefresh(attrs)) return "";
|
|
320
|
+
const pairs = buildAttrString(attrs);
|
|
321
|
+
return pairs ? `<meta ${pairs} />` : "";
|
|
322
|
+
}).filter(Boolean).join("\n ");
|
|
323
|
+
const linkTags = (options.links || []).map((attrs) => {
|
|
324
|
+
const pairs = buildAttrString(attrs);
|
|
325
|
+
return pairs ? `<link ${pairs} />` : "";
|
|
326
|
+
}).filter(Boolean).join("\n ");
|
|
327
|
+
const scriptTags = (options.scripts || []).map((src) => {
|
|
328
|
+
const safe = sanitizeUrl(String(src));
|
|
329
|
+
if (!safe) return "";
|
|
330
|
+
return `<script src="${escapeAttr(safe)}"></script>`;
|
|
331
|
+
}).filter(Boolean).join("\n ");
|
|
332
|
+
const bodyAttrPairs = buildAttrString(options.bodyAttrs);
|
|
333
|
+
const bodyAttrs = bodyAttrPairs ? ` ${bodyAttrPairs}` : "";
|
|
144
334
|
return `<!DOCTYPE html>
|
|
145
335
|
<html>
|
|
146
336
|
<head>
|
|
@@ -173,18 +363,34 @@ async function* renderToStream(element) {
|
|
|
173
363
|
return;
|
|
174
364
|
}
|
|
175
365
|
if (element.nodeType === 8) {
|
|
176
|
-
|
|
177
|
-
yield `<!--${content}-->`;
|
|
366
|
+
yield `<!--${safeCommentText(element.textContent || "")}-->`;
|
|
178
367
|
return;
|
|
179
368
|
}
|
|
180
369
|
if (!(element instanceof HTMLElement)) {
|
|
181
|
-
yield element.textContent || "";
|
|
370
|
+
yield escapeHtml(element.textContent || "");
|
|
182
371
|
return;
|
|
183
372
|
}
|
|
184
373
|
const tag = element.tagName.toLowerCase();
|
|
374
|
+
if (tag === "script" || tag === "style") {
|
|
375
|
+
if (_isDev7) yield `<!--ssr:${tag}-stripped-->`;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
|
|
379
|
+
if (_isDev7) yield "<!--ssr:invalid-tag-->";
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
185
382
|
let openTag = `<${tag}`;
|
|
186
383
|
for (const attr of Array.from(element.attributes)) {
|
|
187
|
-
|
|
384
|
+
const rawName = attr.name;
|
|
385
|
+
if (!isSafeAttrName(rawName)) continue;
|
|
386
|
+
if (isEventHandlerAttr2(rawName)) continue;
|
|
387
|
+
const lowerName = rawName.toLowerCase();
|
|
388
|
+
let value = attr.value;
|
|
389
|
+
if (URL_ATTRS.has(lowerName)) {
|
|
390
|
+
value = sanitizeUrl(value);
|
|
391
|
+
if (!value) continue;
|
|
392
|
+
}
|
|
393
|
+
openTag += ` ${rawName}="${escapeAttr(value)}"`;
|
|
188
394
|
}
|
|
189
395
|
if (VOID_ELEMENTS.has(tag)) {
|
|
190
396
|
yield `${openTag} />`;
|
|
@@ -232,8 +438,9 @@ function hydrateIslands(container, islands) {
|
|
|
232
438
|
const markers = container.querySelectorAll("[data-sibu-island]");
|
|
233
439
|
for (const marker2 of Array.from(markers)) {
|
|
234
440
|
const id = marker2.getAttribute("data-sibu-island") ?? "";
|
|
441
|
+
if (!Object.hasOwn(islands, id)) continue;
|
|
235
442
|
const factory = islands[id];
|
|
236
|
-
if (
|
|
443
|
+
if (typeof factory !== "function") continue;
|
|
237
444
|
const clientTree = factory();
|
|
238
445
|
hydrateNode(marker2, clientTree);
|
|
239
446
|
marker2.setAttribute("data-sibu-hydrated", "true");
|
|
@@ -245,8 +452,9 @@ function hydrateProgressively(container, islands, options) {
|
|
|
245
452
|
const cleanups = [];
|
|
246
453
|
for (const marker2 of Array.from(markers)) {
|
|
247
454
|
const id = marker2.getAttribute("data-sibu-island") ?? "";
|
|
455
|
+
if (!Object.hasOwn(islands, id)) continue;
|
|
248
456
|
const factory = islands[id];
|
|
249
|
-
if (
|
|
457
|
+
if (typeof factory !== "function") continue;
|
|
250
458
|
const observer = new IntersectionObserver(
|
|
251
459
|
(entries) => {
|
|
252
460
|
for (const entry of entries) {
|
|
@@ -285,22 +493,30 @@ function ssrSuspense(props) {
|
|
|
285
493
|
return { element: wrapper, promise };
|
|
286
494
|
}
|
|
287
495
|
function suspenseSwapScript(id, nonce) {
|
|
288
|
-
|
|
496
|
+
if (!SAFE_SUSPENSE_ID.test(id)) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`[SibuJS SSR] suspenseSwapScript: id must match [A-Za-z0-9_-]+ (got: ${JSON.stringify(id.slice(0, 32))})`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
289
501
|
const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
290
|
-
return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${
|
|
502
|
+
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>`;
|
|
291
503
|
}
|
|
292
|
-
async function* renderToSuspenseStream(element, pendingBoundaries = []) {
|
|
504
|
+
async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
|
|
293
505
|
yield* renderToStream(element);
|
|
294
506
|
if (pendingBoundaries.length > 0) {
|
|
295
507
|
const resolved = await Promise.all(pendingBoundaries);
|
|
296
508
|
for (const { id, html: html2 } of resolved) {
|
|
297
|
-
|
|
298
|
-
yield
|
|
509
|
+
if (!SAFE_SUSPENSE_ID.test(id)) continue;
|
|
510
|
+
yield `<div hidden id="sibu-resolved-${id}">${html2}</div>`;
|
|
511
|
+
yield suspenseSwapScript(id, options?.nonce);
|
|
299
512
|
}
|
|
300
513
|
}
|
|
301
514
|
}
|
|
515
|
+
function escapeScriptJson(json) {
|
|
516
|
+
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
517
|
+
}
|
|
302
518
|
function serializeState(state, nonce) {
|
|
303
|
-
const json = JSON.stringify(state)
|
|
519
|
+
const json = escapeScriptJson(JSON.stringify(state));
|
|
304
520
|
const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
305
521
|
return `<script${nonceAttr}>window.${SSR_DATA_ATTR}=${json}</script>`;
|
|
306
522
|
}
|
|
@@ -315,14 +531,30 @@ function escapeHtml(str) {
|
|
|
315
531
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
316
532
|
}
|
|
317
533
|
function escapeAttr(str) {
|
|
318
|
-
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
534
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
319
535
|
}
|
|
320
|
-
var _isDev7, VOID_ELEMENTS, suspenseIdCounter, SSR_DATA_ATTR;
|
|
536
|
+
var _isDev7, SAFE_ATTR_NAME, URL_ATTRS, VOID_ELEMENTS, suspenseIdCounter, SAFE_SUSPENSE_ID, SSR_DATA_ATTR;
|
|
321
537
|
var init_ssr = __esm({
|
|
322
538
|
"src/platform/ssr.ts"() {
|
|
323
539
|
"use strict";
|
|
324
540
|
init_dev();
|
|
541
|
+
init_sanitize();
|
|
325
542
|
_isDev7 = isDev();
|
|
543
|
+
SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
|
|
544
|
+
URL_ATTRS = /* @__PURE__ */ new Set([
|
|
545
|
+
"href",
|
|
546
|
+
"src",
|
|
547
|
+
"action",
|
|
548
|
+
"formaction",
|
|
549
|
+
"cite",
|
|
550
|
+
"poster",
|
|
551
|
+
"background",
|
|
552
|
+
"srcset",
|
|
553
|
+
"ping",
|
|
554
|
+
"manifest",
|
|
555
|
+
"data",
|
|
556
|
+
"xlink:href"
|
|
557
|
+
]);
|
|
326
558
|
VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
327
559
|
"area",
|
|
328
560
|
"base",
|
|
@@ -340,6 +572,7 @@ var init_ssr = __esm({
|
|
|
340
572
|
"wbr"
|
|
341
573
|
]);
|
|
342
574
|
suspenseIdCounter = 0;
|
|
575
|
+
SAFE_SUSPENSE_ID = /^[A-Za-z0-9_-]+$/;
|
|
343
576
|
SSR_DATA_ATTR = "__SIBU_SSR_DATA__";
|
|
344
577
|
}
|
|
345
578
|
});
|
|
@@ -424,28 +657,7 @@ module.exports = __toCommonJS(plugins_exports);
|
|
|
424
657
|
|
|
425
658
|
// src/reactivity/bindAttribute.ts
|
|
426
659
|
init_dev();
|
|
427
|
-
|
|
428
|
-
// src/utils/sanitize.ts
|
|
429
|
-
function sanitizeUrl(url) {
|
|
430
|
-
const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
|
|
431
|
-
if (!trimmed) return "";
|
|
432
|
-
const lower = trimmed.toLowerCase();
|
|
433
|
-
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
|
|
434
|
-
return "";
|
|
435
|
-
}
|
|
436
|
-
return trimmed;
|
|
437
|
-
}
|
|
438
|
-
function sanitizeCSSValue(value) {
|
|
439
|
-
const lower = value.toLowerCase().replace(/\s+/g, "");
|
|
440
|
-
if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("-moz-binding")) {
|
|
441
|
-
return "";
|
|
442
|
-
}
|
|
443
|
-
return value;
|
|
444
|
-
}
|
|
445
|
-
var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "cite", "poster", "background", "srcset"]);
|
|
446
|
-
function isUrlAttribute(attr) {
|
|
447
|
-
return URL_ATTRIBUTES.has(attr);
|
|
448
|
-
}
|
|
660
|
+
init_sanitize();
|
|
449
661
|
|
|
450
662
|
// src/reactivity/track.ts
|
|
451
663
|
init_dev();
|
|
@@ -655,7 +867,20 @@ function cleanup(subscriber) {
|
|
|
655
867
|
|
|
656
868
|
// src/reactivity/bindAttribute.ts
|
|
657
869
|
var _isDev3 = isDev();
|
|
870
|
+
function isEventHandlerAttr(name) {
|
|
871
|
+
if (name.length < 3) return false;
|
|
872
|
+
const lower = name.toLowerCase();
|
|
873
|
+
return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
|
|
874
|
+
}
|
|
658
875
|
function bindAttribute(el, attr, getter) {
|
|
876
|
+
if (isEventHandlerAttr(attr)) {
|
|
877
|
+
if (_isDev3)
|
|
878
|
+
devWarn(
|
|
879
|
+
`bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
|
|
880
|
+
);
|
|
881
|
+
return () => {
|
|
882
|
+
};
|
|
883
|
+
}
|
|
659
884
|
function commit() {
|
|
660
885
|
let value;
|
|
661
886
|
try {
|
|
@@ -699,11 +924,11 @@ function bindChildNode(placeholder, getter) {
|
|
|
699
924
|
if (_isDev4) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
700
925
|
return;
|
|
701
926
|
}
|
|
702
|
-
for (let i2 = 0; i2 < lastNodes.length; i2++) {
|
|
703
|
-
const node = lastNodes[i2];
|
|
704
|
-
if (node.parentNode) node.parentNode.removeChild(node);
|
|
705
|
-
}
|
|
706
927
|
if (result == null || typeof result === "boolean") {
|
|
928
|
+
for (let i2 = 0; i2 < lastNodes.length; i2++) {
|
|
929
|
+
const node = lastNodes[i2];
|
|
930
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
931
|
+
}
|
|
707
932
|
lastNodes.length = 0;
|
|
708
933
|
return;
|
|
709
934
|
}
|
|
@@ -712,28 +937,53 @@ function bindChildNode(placeholder, getter) {
|
|
|
712
937
|
lastNodes.length = 0;
|
|
713
938
|
return;
|
|
714
939
|
}
|
|
715
|
-
|
|
716
|
-
let count = 0;
|
|
940
|
+
let newNodes;
|
|
717
941
|
if (Array.isArray(result)) {
|
|
718
|
-
|
|
942
|
+
newNodes = [];
|
|
719
943
|
for (let i2 = 0; i2 < result.length; i2++) {
|
|
720
944
|
const item = result[i2];
|
|
721
945
|
if (item == null || typeof item === "boolean") continue;
|
|
722
|
-
|
|
723
|
-
parent.insertBefore(node, anchor);
|
|
724
|
-
lastNodes[count++] = node;
|
|
946
|
+
newNodes.push(item instanceof Node ? item : document.createTextNode(String(item)));
|
|
725
947
|
}
|
|
726
948
|
} else {
|
|
727
|
-
if (lastNodes.length < 1) lastNodes = [null];
|
|
728
949
|
const node = result instanceof Node ? result : document.createTextNode(String(result));
|
|
729
|
-
|
|
730
|
-
|
|
950
|
+
newNodes = [node];
|
|
951
|
+
}
|
|
952
|
+
const reused = lastNodes.length > 0 && newNodes.length > 0 ? /* @__PURE__ */ new Set() : void 0;
|
|
953
|
+
if (reused) {
|
|
954
|
+
for (let i2 = 0; i2 < newNodes.length; i2++) {
|
|
955
|
+
for (let j = 0; j < lastNodes.length; j++) {
|
|
956
|
+
if (newNodes[i2] === lastNodes[j]) {
|
|
957
|
+
reused.add(newNodes[i2]);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
for (let i2 = 0; i2 < lastNodes.length; i2++) {
|
|
964
|
+
const node = lastNodes[i2];
|
|
965
|
+
if (reused?.has(node)) continue;
|
|
966
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
967
|
+
}
|
|
968
|
+
const anchor = placeholder.nextSibling;
|
|
969
|
+
for (let i2 = 0; i2 < newNodes.length; i2++) {
|
|
970
|
+
const node = newNodes[i2];
|
|
971
|
+
if (reused?.has(node) && node.parentNode === parent) {
|
|
972
|
+
if (node.nextSibling !== anchor) {
|
|
973
|
+
parent.insertBefore(node, anchor);
|
|
974
|
+
}
|
|
975
|
+
} else {
|
|
976
|
+
parent.insertBefore(node, anchor);
|
|
977
|
+
}
|
|
731
978
|
}
|
|
732
|
-
lastNodes
|
|
979
|
+
lastNodes = newNodes;
|
|
733
980
|
}
|
|
734
981
|
return track(commit);
|
|
735
982
|
}
|
|
736
983
|
|
|
984
|
+
// src/core/rendering/tagFactory.ts
|
|
985
|
+
init_sanitize();
|
|
986
|
+
|
|
737
987
|
// src/core/rendering/dispose.ts
|
|
738
988
|
init_dev();
|
|
739
989
|
var elementDisposers = /* @__PURE__ */ new WeakMap();
|
|
@@ -899,16 +1149,20 @@ function appendChildren(el, nodes) {
|
|
|
899
1149
|
var tagFactory = (tag, ns) => (first, second) => {
|
|
900
1150
|
const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
|
|
901
1151
|
if (first === void 0) return el;
|
|
902
|
-
if (
|
|
1152
|
+
if (typeof first === "string") {
|
|
1153
|
+
if (second !== void 0) {
|
|
1154
|
+
el.setAttribute("class", first);
|
|
1155
|
+
appendChildren(el, second);
|
|
1156
|
+
return el;
|
|
1157
|
+
}
|
|
903
1158
|
el.textContent = first;
|
|
904
1159
|
return el;
|
|
905
1160
|
}
|
|
906
|
-
if (
|
|
907
|
-
el.
|
|
908
|
-
appendChildren(el, second);
|
|
1161
|
+
if (typeof first === "number") {
|
|
1162
|
+
el.textContent = String(first);
|
|
909
1163
|
return el;
|
|
910
1164
|
}
|
|
911
|
-
if (Array.isArray(first) || first instanceof Node) {
|
|
1165
|
+
if (Array.isArray(first) || first instanceof Node || typeof first === "function") {
|
|
912
1166
|
appendChildren(el, first);
|
|
913
1167
|
return el;
|
|
914
1168
|
}
|
|
@@ -917,7 +1171,7 @@ var tagFactory = (tag, ns) => (first, second) => {
|
|
|
917
1171
|
if (pClass != null) applyClass(el, pClass);
|
|
918
1172
|
const pId = props.id;
|
|
919
1173
|
if (pId != null) el.id = pId;
|
|
920
|
-
const pNodes = props.nodes;
|
|
1174
|
+
const pNodes = second !== void 0 ? second : props.nodes;
|
|
921
1175
|
if (pNodes != null) appendChildren(el, pNodes);
|
|
922
1176
|
const pOn = props.on;
|
|
923
1177
|
if (pOn) {
|
|
@@ -942,12 +1196,18 @@ var tagFactory = (tag, ns) => (first, second) => {
|
|
|
942
1196
|
// already handled above / below
|
|
943
1197
|
default: {
|
|
944
1198
|
const value = props[key];
|
|
945
|
-
if (value == null
|
|
1199
|
+
if (value == null) continue;
|
|
946
1200
|
if (key[0] === "o" && key[1] === "n") continue;
|
|
947
1201
|
if (typeof value === "function") {
|
|
948
1202
|
registerDisposer(el, bindAttribute(el, key, value));
|
|
949
|
-
} else if (value ===
|
|
950
|
-
el
|
|
1203
|
+
} else if (typeof value === "boolean") {
|
|
1204
|
+
if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
|
|
1205
|
+
el[key] = value;
|
|
1206
|
+
} else if (value) {
|
|
1207
|
+
el.setAttribute(key, "");
|
|
1208
|
+
} else {
|
|
1209
|
+
el.removeAttribute(key);
|
|
1210
|
+
}
|
|
951
1211
|
} else {
|
|
952
1212
|
const str = String(value);
|
|
953
1213
|
el.setAttribute(key, isUrlAttribute(key) ? sanitizeUrl(str) : str);
|
|
@@ -1217,6 +1477,11 @@ function effect(effectFn, options) {
|
|
|
1217
1477
|
}
|
|
1218
1478
|
|
|
1219
1479
|
// src/plugins/router.ts
|
|
1480
|
+
init_sanitize();
|
|
1481
|
+
function isSafeNavigationTarget(path2) {
|
|
1482
|
+
if (path2 === "") return true;
|
|
1483
|
+
return sanitizeUrl(path2) !== "";
|
|
1484
|
+
}
|
|
1220
1485
|
var LRUCache = class {
|
|
1221
1486
|
constructor(maxSize = 100) {
|
|
1222
1487
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -1702,6 +1967,11 @@ var _SibuRouter = class _SibuRouter {
|
|
|
1702
1967
|
try {
|
|
1703
1968
|
await this.navigator.navigate(async (signal2) => {
|
|
1704
1969
|
const targetPath = this.resolvePath(to);
|
|
1970
|
+
if (!isSafeNavigationTarget(targetPath)) {
|
|
1971
|
+
const from2 = this.currentRouteGetter();
|
|
1972
|
+
const toContext2 = this.createRouteContext(targetPath);
|
|
1973
|
+
throw new NavigationFailureError("aborted", from2, toContext2);
|
|
1974
|
+
}
|
|
1705
1975
|
const from = this.currentRouteGetter();
|
|
1706
1976
|
const toContext = this.createRouteContext(targetPath);
|
|
1707
1977
|
if (this.isSameRoute(from, toContext)) {
|
|
@@ -1731,6 +2001,9 @@ var _SibuRouter = class _SibuRouter {
|
|
|
1731
2001
|
const beforeEachResult = await this.guards.runBeforeEach(to, from, signal2);
|
|
1732
2002
|
if (beforeEachResult !== true) {
|
|
1733
2003
|
if (typeof beforeEachResult === "string") {
|
|
2004
|
+
if (!isSafeNavigationTarget(beforeEachResult)) {
|
|
2005
|
+
throw new NavigationFailureError("aborted", from, to);
|
|
2006
|
+
}
|
|
1734
2007
|
return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1);
|
|
1735
2008
|
}
|
|
1736
2009
|
throw new NavigationFailureError("aborted", from, to);
|
|
@@ -1746,6 +2019,9 @@ var _SibuRouter = class _SibuRouter {
|
|
|
1746
2019
|
const result = await guard(to, from);
|
|
1747
2020
|
if (result !== true) {
|
|
1748
2021
|
if (typeof result === "string") {
|
|
2022
|
+
if (!isSafeNavigationTarget(result)) {
|
|
2023
|
+
throw new NavigationFailureError("aborted", from, to);
|
|
2024
|
+
}
|
|
1749
2025
|
return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1);
|
|
1750
2026
|
}
|
|
1751
2027
|
throw new NavigationFailureError("aborted", from, to);
|
|
@@ -1760,12 +2036,18 @@ var _SibuRouter = class _SibuRouter {
|
|
|
1760
2036
|
`[SibuJS Router] Redirect to absolute URL "${redirectPath}" detected. Use relative paths for safer redirects.`
|
|
1761
2037
|
);
|
|
1762
2038
|
}
|
|
2039
|
+
if (typeof redirectPath === "string" && !isSafeNavigationTarget(redirectPath)) {
|
|
2040
|
+
throw new NavigationFailureError("aborted", from, to);
|
|
2041
|
+
}
|
|
1763
2042
|
return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1);
|
|
1764
2043
|
}
|
|
1765
2044
|
}
|
|
1766
2045
|
const beforeResolveResult = await this.guards.runBeforeResolve(to, from, signal2);
|
|
1767
2046
|
if (beforeResolveResult !== true) {
|
|
1768
2047
|
if (typeof beforeResolveResult === "string") {
|
|
2048
|
+
if (!isSafeNavigationTarget(beforeResolveResult)) {
|
|
2049
|
+
throw new NavigationFailureError("aborted", from, to);
|
|
2050
|
+
}
|
|
1769
2051
|
return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1);
|
|
1770
2052
|
}
|
|
1771
2053
|
throw new NavigationFailureError("aborted", from, to);
|
|
@@ -1925,13 +2207,31 @@ var NavigationFailureError = class extends Error {
|
|
|
1925
2207
|
}
|
|
1926
2208
|
};
|
|
1927
2209
|
var globalRouter = null;
|
|
2210
|
+
function normalizeRoutes(routes) {
|
|
2211
|
+
return routes.map((route2) => {
|
|
2212
|
+
const normalizedChildren = route2.children && route2.children.length > 0 ? normalizeRoutes(route2.children) : route2.children;
|
|
2213
|
+
if ("lazy" in route2 && typeof route2.lazy === "function") {
|
|
2214
|
+
const { lazy: importFn, ...rest } = route2;
|
|
2215
|
+
const asyncRoute = {
|
|
2216
|
+
...rest,
|
|
2217
|
+
component: lazy(importFn),
|
|
2218
|
+
children: normalizedChildren
|
|
2219
|
+
};
|
|
2220
|
+
return asyncRoute;
|
|
2221
|
+
}
|
|
2222
|
+
if (normalizedChildren !== route2.children) {
|
|
2223
|
+
return { ...route2, children: normalizedChildren };
|
|
2224
|
+
}
|
|
2225
|
+
return route2;
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
1928
2228
|
function createRouter(routesOrOptions, options = {}) {
|
|
1929
2229
|
if (globalRouter) {
|
|
1930
2230
|
globalRouter.destroy();
|
|
1931
2231
|
}
|
|
1932
2232
|
let routes;
|
|
1933
2233
|
if (Array.isArray(routesOrOptions)) {
|
|
1934
|
-
routes = routesOrOptions;
|
|
2234
|
+
routes = normalizeRoutes(routesOrOptions);
|
|
1935
2235
|
} else {
|
|
1936
2236
|
options = routesOrOptions;
|
|
1937
2237
|
routes = [];
|
|
@@ -1941,7 +2241,7 @@ function createRouter(routesOrOptions, options = {}) {
|
|
|
1941
2241
|
}
|
|
1942
2242
|
function setRoutes(routes) {
|
|
1943
2243
|
if (!globalRouter) throw new Error("Router not initialized. Call createRouter() first.");
|
|
1944
|
-
globalRouter.updateRoutes(routes);
|
|
2244
|
+
globalRouter.updateRoutes(normalizeRoutes(routes));
|
|
1945
2245
|
}
|
|
1946
2246
|
function route() {
|
|
1947
2247
|
if (!globalRouter) throw new Error("Router not initialized. Call createRouter() first.");
|
|
@@ -2522,6 +2822,20 @@ function createMemoryRouter(routes, _initialPath = "/") {
|
|
|
2522
2822
|
|
|
2523
2823
|
// src/plugins/routerSSR.ts
|
|
2524
2824
|
init_ssr();
|
|
2825
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2826
|
+
function isForbiddenKey(key) {
|
|
2827
|
+
return FORBIDDEN_KEYS.has(key);
|
|
2828
|
+
}
|
|
2829
|
+
function safeDecode(raw) {
|
|
2830
|
+
try {
|
|
2831
|
+
return decodeURIComponent(raw);
|
|
2832
|
+
} catch {
|
|
2833
|
+
return raw;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
function nullObject() {
|
|
2837
|
+
return /* @__PURE__ */ Object.create(null);
|
|
2838
|
+
}
|
|
2525
2839
|
function parseURL(url) {
|
|
2526
2840
|
let remaining = url;
|
|
2527
2841
|
let hash = "";
|
|
@@ -2537,19 +2851,23 @@ function parseURL(url) {
|
|
|
2537
2851
|
remaining = remaining.slice(0, queryIndex);
|
|
2538
2852
|
}
|
|
2539
2853
|
const path2 = remaining || "/";
|
|
2540
|
-
const query =
|
|
2854
|
+
const query = nullObject();
|
|
2541
2855
|
if (queryString) {
|
|
2542
2856
|
const pairs = queryString.split("&");
|
|
2543
2857
|
for (const pair of pairs) {
|
|
2544
2858
|
if (!pair) continue;
|
|
2545
2859
|
const eqIndex = pair.indexOf("=");
|
|
2860
|
+
let key;
|
|
2861
|
+
let value;
|
|
2546
2862
|
if (eqIndex === -1) {
|
|
2547
|
-
|
|
2863
|
+
key = safeDecode(pair);
|
|
2864
|
+
value = "";
|
|
2548
2865
|
} else {
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
query[key] = value;
|
|
2866
|
+
key = safeDecode(pair.slice(0, eqIndex));
|
|
2867
|
+
value = safeDecode(pair.slice(eqIndex + 1));
|
|
2552
2868
|
}
|
|
2869
|
+
if (isForbiddenKey(key)) continue;
|
|
2870
|
+
query[key] = value;
|
|
2553
2871
|
}
|
|
2554
2872
|
}
|
|
2555
2873
|
return { path: path2, query, hash };
|
|
@@ -2607,10 +2925,12 @@ function matchRoute(path2, routes, parentPath = "", parentChain = []) {
|
|
|
2607
2925
|
const compiled = compilePattern(fullPath);
|
|
2608
2926
|
const match = path2.match(compiled.regex);
|
|
2609
2927
|
if (match) {
|
|
2610
|
-
const params =
|
|
2928
|
+
const params = nullObject();
|
|
2611
2929
|
for (let i2 = 0; i2 < compiled.keys.length; i2++) {
|
|
2930
|
+
const key = compiled.keys[i2];
|
|
2931
|
+
if (isForbiddenKey(key)) continue;
|
|
2612
2932
|
if (match[i2 + 1] !== void 0) {
|
|
2613
|
-
params[
|
|
2933
|
+
params[key] = safeDecode(match[i2 + 1]);
|
|
2614
2934
|
}
|
|
2615
2935
|
}
|
|
2616
2936
|
return {
|
|
@@ -2645,7 +2965,7 @@ function resolveServerRouteInternal(url, routes, depth) {
|
|
|
2645
2965
|
return {
|
|
2646
2966
|
route: {
|
|
2647
2967
|
path: normalizedPath,
|
|
2648
|
-
params:
|
|
2968
|
+
params: nullObject(),
|
|
2649
2969
|
query,
|
|
2650
2970
|
hash,
|
|
2651
2971
|
meta: {}
|
|
@@ -2705,20 +3025,26 @@ function renderRouteToString(url, routes, _options) {
|
|
|
2705
3025
|
function renderRouteToDocument(url, routes, options) {
|
|
2706
3026
|
const { html: html2, state } = renderRouteToString(url, routes, options);
|
|
2707
3027
|
const opts = options || {};
|
|
2708
|
-
const metaTags = (opts.meta || []).map(
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
3028
|
+
const metaTags = (opts.meta || []).map((attrs) => {
|
|
3029
|
+
const pairs = buildSafeAttrString(attrs);
|
|
3030
|
+
return pairs ? `<meta ${pairs} />` : "";
|
|
3031
|
+
}).filter(Boolean).join("\n ");
|
|
3032
|
+
const linkTags = (opts.links || []).map((attrs) => {
|
|
3033
|
+
const pairs = buildSafeAttrString(attrs);
|
|
3034
|
+
return pairs ? `<link ${pairs} />` : "";
|
|
3035
|
+
}).filter(Boolean).join("\n ");
|
|
3036
|
+
const scriptTags = (opts.scripts || []).map((src) => {
|
|
3037
|
+
const safe = sanitizeUrlLocal(String(src));
|
|
3038
|
+
if (!safe) return "";
|
|
3039
|
+
return `<script src="${escapeAttrLocal(safe)}"></script>`;
|
|
3040
|
+
}).filter(Boolean).join("\n ");
|
|
3041
|
+
const stateScript = serializeRouteState(state, opts.nonce);
|
|
2716
3042
|
return `<!DOCTYPE html>
|
|
2717
3043
|
<html>
|
|
2718
3044
|
<head>
|
|
2719
3045
|
<meta charset="UTF-8" />
|
|
2720
3046
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2721
|
-
${opts.title ? `<title>${
|
|
3047
|
+
${opts.title ? `<title>${escapeHtmlLocal(opts.title)}</title>` : ""}
|
|
2722
3048
|
${metaTags}
|
|
2723
3049
|
${linkTags}
|
|
2724
3050
|
${opts.headExtra || ""}
|
|
@@ -2730,9 +3056,10 @@ function renderRouteToDocument(url, routes, options) {
|
|
|
2730
3056
|
</body>
|
|
2731
3057
|
</html>`;
|
|
2732
3058
|
}
|
|
2733
|
-
function serializeRouteState(state) {
|
|
2734
|
-
const json = JSON.stringify(state)
|
|
2735
|
-
|
|
3059
|
+
function serializeRouteState(state, nonce) {
|
|
3060
|
+
const json = escapeScriptJson(JSON.stringify(state));
|
|
3061
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttrLocal(nonce)}"` : "";
|
|
3062
|
+
return `<script${nonceAttr}>window.${SSR_ROUTE_STATE_KEY}=${json}</script>`;
|
|
2736
3063
|
}
|
|
2737
3064
|
function deserializeRouteState() {
|
|
2738
3065
|
if (typeof window === "undefined") return void 0;
|
|
@@ -2770,11 +3097,59 @@ function createSSRRouter(routes) {
|
|
|
2770
3097
|
}
|
|
2771
3098
|
};
|
|
2772
3099
|
}
|
|
2773
|
-
|
|
3100
|
+
var SAFE_ATTR_NAME2 = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
|
|
3101
|
+
function isSafeAttrName2(name) {
|
|
3102
|
+
return SAFE_ATTR_NAME2.test(name);
|
|
3103
|
+
}
|
|
3104
|
+
function isEventHandlerAttr3(name) {
|
|
3105
|
+
if (name.length < 3) return false;
|
|
3106
|
+
const lower = name.toLowerCase();
|
|
3107
|
+
return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
|
|
3108
|
+
}
|
|
3109
|
+
var URL_ATTRS2 = /* @__PURE__ */ new Set([
|
|
3110
|
+
"href",
|
|
3111
|
+
"src",
|
|
3112
|
+
"action",
|
|
3113
|
+
"formaction",
|
|
3114
|
+
"cite",
|
|
3115
|
+
"poster",
|
|
3116
|
+
"background",
|
|
3117
|
+
"srcset",
|
|
3118
|
+
"ping",
|
|
3119
|
+
"manifest",
|
|
3120
|
+
"data",
|
|
3121
|
+
"xlink:href"
|
|
3122
|
+
]);
|
|
3123
|
+
function sanitizeUrlLocal(url) {
|
|
3124
|
+
const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
|
|
3125
|
+
if (!trimmed) return "";
|
|
3126
|
+
const lower = trimmed.toLowerCase();
|
|
3127
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
|
|
3128
|
+
return "";
|
|
3129
|
+
}
|
|
3130
|
+
return trimmed;
|
|
3131
|
+
}
|
|
3132
|
+
function buildSafeAttrString(attrs) {
|
|
3133
|
+
const out = [];
|
|
3134
|
+
for (const rawKey of Object.keys(attrs)) {
|
|
3135
|
+
if (!Object.hasOwn(attrs, rawKey)) continue;
|
|
3136
|
+
if (!isSafeAttrName2(rawKey)) continue;
|
|
3137
|
+
if (isEventHandlerAttr3(rawKey)) continue;
|
|
3138
|
+
const lowerKey = rawKey.toLowerCase();
|
|
3139
|
+
let value = String(attrs[rawKey]);
|
|
3140
|
+
if (URL_ATTRS2.has(lowerKey)) {
|
|
3141
|
+
value = sanitizeUrlLocal(value);
|
|
3142
|
+
if (!value) continue;
|
|
3143
|
+
}
|
|
3144
|
+
out.push(`${rawKey}="${escapeAttrLocal(value)}"`);
|
|
3145
|
+
}
|
|
3146
|
+
return out.join(" ");
|
|
3147
|
+
}
|
|
3148
|
+
function escapeHtmlLocal(str) {
|
|
2774
3149
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2775
3150
|
}
|
|
2776
|
-
function
|
|
2777
|
-
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
3151
|
+
function escapeAttrLocal(str) {
|
|
3152
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
2778
3153
|
}
|
|
2779
3154
|
|
|
2780
3155
|
// src/plugins/plugin.ts
|