@ilha/router 0.9.2 → 0.10.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 +44 -27
- package/dist/head.d.ts +67 -0
- package/dist/http.d.ts +28 -0
- package/dist/index.d.ts +43 -97
- package/dist/index.js +2 -1
- package/dist/{plugin-BHuojFhQ.js → plugin-CmI3Brr2.js} +102 -115
- package/dist/plugin.d.ts +1 -1
- package/dist/route-match.d.ts +1 -1
- package/dist/{rspack.d.ts → rsbuild.d.ts} +2 -2
- package/dist/rsbuild.js +10 -0
- package/dist/server-island.d.ts +17 -2
- package/dist/server-island.js +13 -25
- package/dist/server-islands.d.ts +16 -2
- package/dist/snapshot-CsEaY6h_.js +337 -0
- package/dist/snapshot.d.ts +1 -0
- package/dist/{src-BBsbD5vU.js → src-B5dHU24f.js} +157 -361
- package/dist/ssr-BxrcUYy5.js +498 -0
- package/dist/ssr.d.ts +173 -0
- package/dist/ssr.js +2 -200
- package/dist/vite.js +1 -1
- package/package.json +7 -14
- package/dist/public-types.d.ts +0 -7
- package/dist/request-scope-C4reU4v0.js +0 -34
- package/dist/rolldown.d.ts +0 -6
- package/dist/rolldown.js +0 -10
- package/dist/rspack.js +0 -10
- package/dist/server-island-registry.d.ts +0 -122
- package/dist/server-island-registry.js +0 -189
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as escapeHtml, c as withHeadStore, i as escapeHeadAttr, n as applyHeadEntriesToDocument, o as head, r as cssEscapeAttr, s as serializeHead, t as parseSnapshotAttr } from "./snapshot-CsEaY6h_.js";
|
|
2
|
+
import { context, effect, html, ilha, mount, state } from "ilha";
|
|
3
|
+
import { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL } from "ilha/internal";
|
|
2
4
|
|
|
3
5
|
//#region src/hash.ts
|
|
4
6
|
const isBrowser$1 = typeof window !== "undefined" && typeof document !== "undefined";
|
|
@@ -83,6 +85,7 @@ const hashAdapter = {
|
|
|
83
85
|
extractLogicalPath(anchor) {
|
|
84
86
|
const href = anchor.getAttribute("href");
|
|
85
87
|
if (!href) return null;
|
|
88
|
+
if (href.startsWith("//")) return null;
|
|
86
89
|
if (anchor.protocol && !/^(http:|https:)$/.test(anchor.protocol)) return null;
|
|
87
90
|
if (href.startsWith("#")) {
|
|
88
91
|
const inner = href.slice(1);
|
|
@@ -129,6 +132,37 @@ function getAdapter() {
|
|
|
129
132
|
return _adapter;
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/http.ts
|
|
137
|
+
/**
|
|
138
|
+
* Build an HTTP `Response` for SSR output with sensible security headers:
|
|
139
|
+
* `Content-Type: text/html`, `X-Content-Type-Options: nosniff`,
|
|
140
|
+
* `Referrer-Policy`, `Cache-Control: no-store`, and an optional CSP. This is
|
|
141
|
+
* a low-level helper — prefer {@link RouterBuilder.respond} for the full
|
|
142
|
+
* render+head+headers pipeline.
|
|
143
|
+
*/
|
|
144
|
+
function httpResponse(body, options = {}) {
|
|
145
|
+
const headers = new Headers(options.headers);
|
|
146
|
+
if (body != null && !headers.has("content-type")) headers.set("content-type", "text/html; charset=utf-8");
|
|
147
|
+
if (!headers.has("x-content-type-options")) headers.set("x-content-type-options", "nosniff");
|
|
148
|
+
if (!headers.has("referrer-policy")) headers.set("referrer-policy", "no-referrer");
|
|
149
|
+
if (!headers.has("cache-control")) headers.set("cache-control", "no-store");
|
|
150
|
+
const csp = options.contentSecurityPolicy ?? (options.cspNonce ? `default-src 'self'; script-src 'self' 'nonce-${options.cspNonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'` : void 0);
|
|
151
|
+
if (csp) headers.set("content-security-policy", csp);
|
|
152
|
+
return new Response(body, {
|
|
153
|
+
status: options.status ?? 200,
|
|
154
|
+
headers
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const EMPTY_HEAD = {
|
|
158
|
+
headTags: "",
|
|
159
|
+
htmlAttrs: "",
|
|
160
|
+
bodyAttrs: ""
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Options for {@link RouterBuilder.respond}.
|
|
164
|
+
*/
|
|
165
|
+
|
|
132
166
|
//#endregion
|
|
133
167
|
//#region src/route-match.ts
|
|
134
168
|
function parsePattern(pattern) {
|
|
@@ -247,7 +281,12 @@ function composeLoaders(loaders) {
|
|
|
247
281
|
if (loaders.length === 1) return loaders[0];
|
|
248
282
|
return async (ctx) => {
|
|
249
283
|
const results = await Promise.all(loaders.map((l) => l(ctx)));
|
|
250
|
-
|
|
284
|
+
let merged = {};
|
|
285
|
+
for (const r of results) merged = {
|
|
286
|
+
...merged,
|
|
287
|
+
...r
|
|
288
|
+
};
|
|
289
|
+
return merged;
|
|
251
290
|
};
|
|
252
291
|
}
|
|
253
292
|
const WRAP_LAYOUT_LEAF = Symbol.for("ilha.router.wrapLayout.leaf");
|
|
@@ -375,13 +414,21 @@ function injectKPageSlot(layoutHtml, slotInnerHtml, which) {
|
|
|
375
414
|
const target = which === "innermost" ? spans[spans.length - 1] : spans[0];
|
|
376
415
|
return layoutHtml.slice(0, target.openEnd) + slotInnerHtml + layoutHtml.slice(target.closeStart);
|
|
377
416
|
}
|
|
417
|
+
/** Branded read of ilha's optional mount-handle hook (absent on plain islands). */
|
|
418
|
+
function readMountInternal(island) {
|
|
419
|
+
return island[ISLAND_MOUNT_INTERNAL];
|
|
420
|
+
}
|
|
421
|
+
/** Branded write of ilha's mount-handle hook — mirrors the core island contract. */
|
|
422
|
+
function writeMountInternal(island, hook) {
|
|
423
|
+
island[ISLAND_MOUNT_INTERNAL] = hook;
|
|
424
|
+
}
|
|
378
425
|
function layoutHtmlWithEmptyKPage(wrappedLayout, props) {
|
|
379
426
|
const handler = wrappedLayout[WRAP_LAYOUT_HANDLER];
|
|
380
427
|
if (!handler) return wrappedLayout.toString(props);
|
|
381
428
|
const rawKeyed = (wrappedLayout[WRAP_LAYOUT_LEAF] ?? wrappedLayout).key("page");
|
|
382
429
|
const shellChild = ((partial) => rawKeyed({
|
|
383
430
|
...props,
|
|
384
|
-
...partial
|
|
431
|
+
...partial
|
|
385
432
|
}));
|
|
386
433
|
Object.assign(shellChild, { toString: () => "" });
|
|
387
434
|
shellChild[ISLAND_CALL] = true;
|
|
@@ -400,7 +447,7 @@ function wrapLayout(layout, page) {
|
|
|
400
447
|
const merged = layoutInputRef.merged;
|
|
401
448
|
const slotProps = merged && typeof merged === "object" ? {
|
|
402
449
|
...merged,
|
|
403
|
-
...props
|
|
450
|
+
...props
|
|
404
451
|
} : props;
|
|
405
452
|
return rawKeyedPage(slotProps);
|
|
406
453
|
});
|
|
@@ -427,33 +474,41 @@ function wrapLayout(layout, page) {
|
|
|
427
474
|
};
|
|
428
475
|
if (mountHost.hasAttribute("data-ilha-state")) {
|
|
429
476
|
const outerState = outer.getAttribute("data-ilha-state");
|
|
430
|
-
if (outerState)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
477
|
+
if (outerState) {
|
|
478
|
+
const outerParsed = parseSnapshotAttr(outerState);
|
|
479
|
+
if (outerParsed) {
|
|
480
|
+
applyOuterSnapshot(outerParsed);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
434
484
|
const slotState = mountHost.getAttribute("data-ilha-state");
|
|
435
|
-
if (slotState)
|
|
436
|
-
const snapshot =
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
485
|
+
if (slotState) {
|
|
486
|
+
const snapshot = parseSnapshotAttr(slotState);
|
|
487
|
+
if (snapshot) {
|
|
488
|
+
delete snapshot._skipOnMount;
|
|
489
|
+
mountHost.setAttribute("data-ilha-state", JSON.stringify(snapshot));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
440
492
|
return;
|
|
441
493
|
}
|
|
442
494
|
const outerState = outer.getAttribute("data-ilha-state");
|
|
443
|
-
if (outerState)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
495
|
+
if (outerState) {
|
|
496
|
+
const outerParsed = parseSnapshotAttr(outerState);
|
|
497
|
+
if (outerParsed) {
|
|
498
|
+
applyOuterSnapshot(outerParsed);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (mountHost.childNodes.length > 0) mountHost.setAttribute("data-ilha-state", "{\"v\":2}");
|
|
448
503
|
}
|
|
449
504
|
function wrapLeafPageMountHooks(leaf) {
|
|
450
|
-
const leafInternal = leaf
|
|
505
|
+
const leafInternal = readMountInternal(leaf);
|
|
451
506
|
if (typeof leafInternal !== "function") return;
|
|
452
|
-
leaf
|
|
507
|
+
writeMountInternal(leaf, (host, props) => {
|
|
453
508
|
const outer = host.closest("[data-ilha]");
|
|
454
509
|
if (outer && outer !== host) preparePageMountHost(outer, host);
|
|
455
510
|
return leafInternal(host, props);
|
|
456
|
-
};
|
|
511
|
+
});
|
|
457
512
|
const leafMount = leaf.mount.bind(leaf);
|
|
458
513
|
leaf.mount = (host, props) => {
|
|
459
514
|
const outer = host.closest("[data-ilha]");
|
|
@@ -463,8 +518,8 @@ function wrapLayout(layout, page) {
|
|
|
463
518
|
}
|
|
464
519
|
wrapLeafPageMountHooks(leafPage);
|
|
465
520
|
const pageHandles = /* @__PURE__ */ new Map();
|
|
466
|
-
const pageInternalBase = page
|
|
467
|
-
if (typeof pageInternalBase === "function") page
|
|
521
|
+
const pageInternalBase = readMountInternal(page);
|
|
522
|
+
if (typeof pageInternalBase === "function") writeMountInternal(page, (host, props) => {
|
|
468
523
|
const handle = pageInternalBase(host, props);
|
|
469
524
|
const entry = {
|
|
470
525
|
handle,
|
|
@@ -481,7 +536,7 @@ function wrapLayout(layout, page) {
|
|
|
481
536
|
handle.updateProps(p);
|
|
482
537
|
}
|
|
483
538
|
};
|
|
484
|
-
};
|
|
539
|
+
});
|
|
485
540
|
/**
|
|
486
541
|
* In-place prop update for one mounted layout instance: refresh the
|
|
487
542
|
* merged-input ref (so any layout re-render passes fresh props to `k:page`),
|
|
@@ -494,15 +549,27 @@ function wrapLayout(layout, page) {
|
|
|
494
549
|
const layoutUpdateProps = (layoutHost, coreUpdate) => (p) => {
|
|
495
550
|
setLayoutMergedInput(p);
|
|
496
551
|
for (const [pageHost, entry] of pageHandles) if (layoutHost.contains(pageHost)) entry.handle.updateProps({
|
|
497
|
-
...entry.mountProps
|
|
498
|
-
...p
|
|
552
|
+
...entry.mountProps,
|
|
553
|
+
...p
|
|
499
554
|
});
|
|
500
555
|
coreUpdate?.(p);
|
|
501
556
|
};
|
|
502
557
|
const layoutMount = Wrapped.mount.bind(Wrapped);
|
|
503
|
-
const layoutInternal = Wrapped
|
|
558
|
+
const layoutInternal = readMountInternal(Wrapped);
|
|
504
559
|
function prepareLayoutMountHost(host) {
|
|
505
560
|
preparePageMountHost(host, pageMountHost(host));
|
|
561
|
+
const raw = host.getAttribute("data-ilha-state");
|
|
562
|
+
if (raw) {
|
|
563
|
+
const parsed = parseSnapshotAttr(raw);
|
|
564
|
+
if (parsed) {
|
|
565
|
+
const s = parsed["s"];
|
|
566
|
+
const d = parsed["d"];
|
|
567
|
+
if (!(Array.isArray(s) && s.length > 0 || Array.isArray(d) && d.length > 0) && parsed["_skipOnMount"] === true) {
|
|
568
|
+
delete parsed["_skipOnMount"];
|
|
569
|
+
host.setAttribute("data-ilha-state", JSON.stringify(parsed));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
506
573
|
}
|
|
507
574
|
Wrapped.mount = (host, props) => {
|
|
508
575
|
setLayoutMergedInput(props);
|
|
@@ -515,7 +582,7 @@ function wrapLayout(layout, page) {
|
|
|
515
582
|
});
|
|
516
583
|
return unmount;
|
|
517
584
|
};
|
|
518
|
-
Wrapped
|
|
585
|
+
writeMountInternal(Wrapped, (host, props) => {
|
|
519
586
|
setLayoutMergedInput(props);
|
|
520
587
|
prepareLayoutMountHost(host);
|
|
521
588
|
const base = typeof layoutInternal === "function" ? layoutInternal(host, props) : {
|
|
@@ -528,7 +595,7 @@ function wrapLayout(layout, page) {
|
|
|
528
595
|
};
|
|
529
596
|
ISLAND_MOUNT_HANDLES.set(host, enhanced);
|
|
530
597
|
return enhanced;
|
|
531
|
-
};
|
|
598
|
+
});
|
|
532
599
|
Wrapped.hydratable = async (props, opts) => {
|
|
533
600
|
if (!opts?.name) throw new Error("wrapLayout: hydratable requires options.name");
|
|
534
601
|
const resolvedProps = props ?? {};
|
|
@@ -544,7 +611,7 @@ function wrapLayout(layout, page) {
|
|
|
544
611
|
return Wrapped;
|
|
545
612
|
}
|
|
546
613
|
function wrapError(handler, page) {
|
|
547
|
-
const Wrapper = ilha
|
|
614
|
+
const Wrapper = ilha(() => {
|
|
548
615
|
try {
|
|
549
616
|
return page.toString();
|
|
550
617
|
} catch (e) {
|
|
@@ -580,9 +647,9 @@ function wrapError(handler, page) {
|
|
|
580
647
|
return errorIsland.mount(host, props);
|
|
581
648
|
}
|
|
582
649
|
};
|
|
583
|
-
Wrapper
|
|
650
|
+
writeMountInternal(Wrapper, (host, props) => {
|
|
584
651
|
try {
|
|
585
|
-
const pageInternal = page
|
|
652
|
+
const pageInternal = readMountInternal(page);
|
|
586
653
|
if (typeof pageInternal === "function") return pageInternal(host, props);
|
|
587
654
|
return {
|
|
588
655
|
unmount: page.mount(host, props),
|
|
@@ -606,7 +673,7 @@ function wrapError(handler, page) {
|
|
|
606
673
|
updateProps: () => {}
|
|
607
674
|
};
|
|
608
675
|
}
|
|
609
|
-
};
|
|
676
|
+
});
|
|
610
677
|
Wrapper.hydratable = async (props, opts) => {
|
|
611
678
|
if (!opts?.name) throw new Error("wrapError: hydratable requires options.name");
|
|
612
679
|
return page.hydratable(props ?? {}, opts);
|
|
@@ -772,7 +839,7 @@ const noopRouteHandle = () => ({
|
|
|
772
839
|
/** Mount an island keeping the full internal handle so later same-island
|
|
773
840
|
* navigations can push new loader props instead of remounting. */
|
|
774
841
|
function mountIslandWithHandle(island, host, props) {
|
|
775
|
-
const internal = island
|
|
842
|
+
const internal = readMountInternal(island);
|
|
776
843
|
if (typeof internal === "function") {
|
|
777
844
|
const h = internal(host, props);
|
|
778
845
|
return {
|
|
@@ -788,11 +855,12 @@ function mountIslandWithHandle(island, host, props) {
|
|
|
788
855
|
/**
|
|
789
856
|
* Same-island fast path: fetch fresh loader data and push it into the mounted
|
|
790
857
|
* island via `updateProps` — ilha's fine-grained morph reconciles the DOM, so
|
|
791
|
-
* focus, caret, selection, and scroll survive (the reason
|
|
792
|
-
* filter inputs don't blur while typing). Returns "updated" when
|
|
793
|
-
* redirected), "remount" when the caller must run the full
|
|
794
|
-
* path (loader error / not-found need boundary DOM; the full
|
|
795
|
-
* accepted for these rare cases). Throws AbortError when
|
|
858
|
+
* focus, caret, selection, and scroll survive (the reason same-route param
|
|
859
|
+
* updates on filter inputs don't blur while typing). Returns "updated" when
|
|
860
|
+
* applied (or redirected), "remount" when the caller must run the full
|
|
861
|
+
* teardown + mount path (loader error / not-found need boundary DOM; the full
|
|
862
|
+
* path re-fetches, accepted for these rare cases). Throws AbortError when
|
|
863
|
+
* superseded.
|
|
796
864
|
*/
|
|
797
865
|
async function updateRouteInPlace(handle, pathWithSearch, signal) {
|
|
798
866
|
if (!handle.updateProps) return "remount";
|
|
@@ -852,7 +920,7 @@ async function mountRouteWithHydration(island, host, pathWithSearch, signal, reg
|
|
|
852
920
|
}));
|
|
853
921
|
const escaped = escapeHtml(loaderResult.message);
|
|
854
922
|
await withViewSwap(() => {
|
|
855
|
-
host.innerHTML = `<div data-router-view data-router-error="${loaderResult.status}">${escaped}</div>`;
|
|
923
|
+
host.innerHTML = `<div data-router-view data-router-error="${escapeHtml(loaderResult.status)}">${escaped}</div>`;
|
|
856
924
|
});
|
|
857
925
|
return noopRouteHandle();
|
|
858
926
|
}
|
|
@@ -907,12 +975,12 @@ function mountLoaderErrorBoundary(boundary, host, status, message) {
|
|
|
907
975
|
search: routeSearch(),
|
|
908
976
|
hash: routeHash()
|
|
909
977
|
});
|
|
910
|
-
host.innerHTML = `<div data-router-view data-router-error="${status}">${errorIsland.toString()}</div>`;
|
|
978
|
+
host.innerHTML = `<div data-router-view data-router-error="${escapeHtml(status)}">${errorIsland.toString()}</div>`;
|
|
911
979
|
const ehHost = host.firstElementChild;
|
|
912
980
|
return ehHost ? errorIsland.mount(ehHost) : () => {};
|
|
913
981
|
} catch (e) {
|
|
914
982
|
console.error("[ilha-router] error boundary threw while rendering a loader error:", e);
|
|
915
|
-
host.innerHTML = `<div data-router-view data-router-error="${status}"></div>`;
|
|
983
|
+
host.innerHTML = `<div data-router-view data-router-error="${escapeHtml(status)}"></div>`;
|
|
916
984
|
return () => {};
|
|
917
985
|
}
|
|
918
986
|
}
|
|
@@ -944,6 +1012,7 @@ const _routePathSig = context("router.path", "");
|
|
|
944
1012
|
const _routeParamsSig = context("router.params", {});
|
|
945
1013
|
const _routeSearchSig = context("router.search", "");
|
|
946
1014
|
const _routeHashSig = context("router.hash", "");
|
|
1015
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
947
1016
|
function routePath(value) {
|
|
948
1017
|
const store = activeRouteStore();
|
|
949
1018
|
if (arguments.length > 0) {
|
|
@@ -952,6 +1021,7 @@ function routePath(value) {
|
|
|
952
1021
|
}
|
|
953
1022
|
return store ? store.path : _routePathSig();
|
|
954
1023
|
}
|
|
1024
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
955
1025
|
function routeParams(value) {
|
|
956
1026
|
const store = activeRouteStore();
|
|
957
1027
|
if (arguments.length > 0) {
|
|
@@ -960,6 +1030,7 @@ function routeParams(value) {
|
|
|
960
1030
|
}
|
|
961
1031
|
return store ? store.params : _routeParamsSig();
|
|
962
1032
|
}
|
|
1033
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
963
1034
|
function routeSearch(value) {
|
|
964
1035
|
const store = activeRouteStore();
|
|
965
1036
|
if (arguments.length > 0) {
|
|
@@ -968,6 +1039,7 @@ function routeSearch(value) {
|
|
|
968
1039
|
}
|
|
969
1040
|
return store ? store.search : _routeSearchSig();
|
|
970
1041
|
}
|
|
1042
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
971
1043
|
function routeHash(value) {
|
|
972
1044
|
const store = activeRouteStore();
|
|
973
1045
|
if (arguments.length > 0) {
|
|
@@ -1276,7 +1348,7 @@ let _notFound = null;
|
|
|
1276
1348
|
/** Redirect policy for browser-executed loaders — mirrors the last router's
|
|
1277
1349
|
* `allowExternalRedirects` option (module-level for the same reason as `_notFound`). */
|
|
1278
1350
|
let _allowExternalRedirects = false;
|
|
1279
|
-
const RouterView = ilha
|
|
1351
|
+
const RouterView = ilha(() => {
|
|
1280
1352
|
const island = activeIsland();
|
|
1281
1353
|
if (!island) {
|
|
1282
1354
|
if (_notFound) return `<div data-router-view data-router-not-found>${_notFound.toString()}</div>`;
|
|
@@ -1284,24 +1356,34 @@ const RouterView = ilha.render(() => {
|
|
|
1284
1356
|
}
|
|
1285
1357
|
return `<div data-router-view>${island.toString()}</div>`;
|
|
1286
1358
|
});
|
|
1287
|
-
const RouterLink = ilha
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1359
|
+
const RouterLink = ilha(({ href = "", label = "" }) => {
|
|
1360
|
+
const hrefState = state(href);
|
|
1361
|
+
const labelState = state(label);
|
|
1362
|
+
effect.once(({ host, signal }) => {
|
|
1363
|
+
const link = host.matches("[data-link]") ? host : host.querySelector("[data-link]");
|
|
1364
|
+
if (!link) return;
|
|
1365
|
+
link.addEventListener("click", (event) => {
|
|
1366
|
+
event.preventDefault();
|
|
1367
|
+
navigate(hrefState());
|
|
1368
|
+
}, { signal });
|
|
1369
|
+
link.addEventListener("mouseenter", () => {
|
|
1370
|
+
const href = hrefState();
|
|
1371
|
+
if (!href) return;
|
|
1372
|
+
if (/^https?:\/\//i.test(href)) try {
|
|
1373
|
+
const u = new URL(href);
|
|
1374
|
+
if (u.origin !== location.origin) return;
|
|
1375
|
+
prefetch(u.pathname + u.search);
|
|
1376
|
+
return;
|
|
1377
|
+
} catch {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
prefetch(href);
|
|
1381
|
+
}, { signal });
|
|
1382
|
+
});
|
|
1383
|
+
return html`<a data-link data-prefetch href="${() => getAdapter().toLinkHref(hrefState())}"
|
|
1384
|
+
>${labelState()}</a
|
|
1385
|
+
>`;
|
|
1386
|
+
});
|
|
1305
1387
|
function isActive(pattern, options = {}) {
|
|
1306
1388
|
if (options.exact === false) {
|
|
1307
1389
|
const path = routePath();
|
|
@@ -1312,291 +1394,6 @@ function isActive(pattern, options = {}) {
|
|
|
1312
1394
|
if (!match) return false;
|
|
1313
1395
|
return match.data.pattern === pattern;
|
|
1314
1396
|
}
|
|
1315
|
-
const ILHA_HEAD_ATTR = "data-ilha-head";
|
|
1316
|
-
const ILHA_ROUTER_HTML_ATTR = "data-ilha-router-html";
|
|
1317
|
-
const ILHA_ROUTER_BODY_ATTR = "data-ilha-router-body";
|
|
1318
|
-
/** Browser-only fallback; SSR uses AsyncLocalStorage (see `withHeadStore`). */
|
|
1319
|
-
let _browserHeadStore = null;
|
|
1320
|
-
let _headAls = null;
|
|
1321
|
-
let _headAlsInit = null;
|
|
1322
|
-
/** ESM dynamic import — Nitro/Vite SSR workers have no `require`. */
|
|
1323
|
-
async function getHeadAlsAsync() {
|
|
1324
|
-
if (_headAls) return _headAls;
|
|
1325
|
-
if (!_headAlsInit) _headAlsInit = import("node:async_hooks").then(({ AsyncLocalStorage }) => {
|
|
1326
|
-
_headAls = new AsyncLocalStorage();
|
|
1327
|
-
return _headAls;
|
|
1328
|
-
});
|
|
1329
|
-
return _headAlsInit;
|
|
1330
|
-
}
|
|
1331
|
-
function activeHeadStore() {
|
|
1332
|
-
if (isBrowser) return _browserHeadStore;
|
|
1333
|
-
return _headAls?.getStore() ?? null;
|
|
1334
|
-
}
|
|
1335
|
-
/**
|
|
1336
|
-
* Contribute `<head>` data from inside an island's `.render()` body or a
|
|
1337
|
-
* layout. During SSR this collects into the active render window; on the
|
|
1338
|
-
* client, entries are collected when the router re-renders a route inside
|
|
1339
|
-
* `withHeadStore` and then applied to `document`. Prefer a loader's `ctx.head`
|
|
1340
|
-
* for data that depends on the request.
|
|
1341
|
-
*/
|
|
1342
|
-
function head(input) {
|
|
1343
|
-
const store = activeHeadStore();
|
|
1344
|
-
if (!store) {
|
|
1345
|
-
if (!isBrowser) console.warn("[ilha-router] head() called outside an SSR render window — ignored.");
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
store.entries.push(input);
|
|
1349
|
-
}
|
|
1350
|
-
function cssEscapeAttr(value) {
|
|
1351
|
-
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
|
|
1352
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
1353
|
-
}
|
|
1354
|
-
function headManagedMetaSelector(tag) {
|
|
1355
|
-
if ("charset" in tag) return `meta[charset][${ILHA_HEAD_ATTR}]`;
|
|
1356
|
-
if ("name" in tag) return `meta[name="${cssEscapeAttr(tag.name)}"][${ILHA_HEAD_ATTR}]`;
|
|
1357
|
-
if ("property" in tag) return `meta[property="${cssEscapeAttr(tag.property)}"][${ILHA_HEAD_ATTR}]`;
|
|
1358
|
-
if ("http-equiv" in tag) return `meta[http-equiv="${cssEscapeAttr(tag["http-equiv"])}"][${ILHA_HEAD_ATTR}]`;
|
|
1359
|
-
return null;
|
|
1360
|
-
}
|
|
1361
|
-
function headManagedLinkSelector(tag) {
|
|
1362
|
-
if (tag.rel && tag.href) return `link[rel="${cssEscapeAttr(tag.rel)}"][href="${cssEscapeAttr(tag.href)}"][${ILHA_HEAD_ATTR}]`;
|
|
1363
|
-
return null;
|
|
1364
|
-
}
|
|
1365
|
-
/**
|
|
1366
|
-
* Apply merged head entries on client navigations. Updates `document.title` and
|
|
1367
|
-
* managed meta/link nodes (`data-ilha-head`). Script tags from HeadInput are
|
|
1368
|
-
* SSR-only and are not re-injected here. Removes managed tags from the previous
|
|
1369
|
-
* route that are not part of this navigation's set.
|
|
1370
|
-
*/
|
|
1371
|
-
function applyHeadEntriesToDocument(entries) {
|
|
1372
|
-
if (!isBrowser) return;
|
|
1373
|
-
let title;
|
|
1374
|
-
let titleTemplate;
|
|
1375
|
-
const meta = [];
|
|
1376
|
-
const link = [];
|
|
1377
|
-
let htmlAttrs = {};
|
|
1378
|
-
let bodyAttrs = {};
|
|
1379
|
-
for (const entry of entries) {
|
|
1380
|
-
if (entry.title !== void 0) title = entry.title;
|
|
1381
|
-
if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
|
|
1382
|
-
if (entry.meta) meta.push(...entry.meta);
|
|
1383
|
-
if (entry.link) link.push(...entry.link);
|
|
1384
|
-
if (entry.htmlAttrs) htmlAttrs = {
|
|
1385
|
-
...htmlAttrs,
|
|
1386
|
-
...entry.htmlAttrs
|
|
1387
|
-
};
|
|
1388
|
-
if (entry.bodyAttrs) bodyAttrs = {
|
|
1389
|
-
...bodyAttrs,
|
|
1390
|
-
...entry.bodyAttrs
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
const resolvedTitle = applyTitleTemplate(title, titleTemplate);
|
|
1394
|
-
if (resolvedTitle !== void 0) document.title = resolvedTitle;
|
|
1395
|
-
const metaTags = dedupByKey(meta, metaDedupKey);
|
|
1396
|
-
const linkTags = dedupByKey(link, (t) => `${t.rel ?? ""}:${t.href ?? ""}`);
|
|
1397
|
-
const keepManaged = /* @__PURE__ */ new Set();
|
|
1398
|
-
for (const tag of metaTags) {
|
|
1399
|
-
const selector = headManagedMetaSelector(tag);
|
|
1400
|
-
if (!selector) continue;
|
|
1401
|
-
let el = document.querySelector(selector);
|
|
1402
|
-
if (!el) {
|
|
1403
|
-
el = document.createElement("meta");
|
|
1404
|
-
el.setAttribute(ILHA_HEAD_ATTR, "");
|
|
1405
|
-
document.head.appendChild(el);
|
|
1406
|
-
}
|
|
1407
|
-
for (const [k, v] of Object.entries(tag)) el.setAttribute(k, v);
|
|
1408
|
-
keepManaged.add(el);
|
|
1409
|
-
}
|
|
1410
|
-
for (const tag of linkTags) {
|
|
1411
|
-
const selector = headManagedLinkSelector(tag);
|
|
1412
|
-
let el = selector ? document.querySelector(selector) : null;
|
|
1413
|
-
if (!el) {
|
|
1414
|
-
el = document.createElement("link");
|
|
1415
|
-
el.setAttribute(ILHA_HEAD_ATTR, "");
|
|
1416
|
-
document.head.appendChild(el);
|
|
1417
|
-
}
|
|
1418
|
-
for (const [k, v] of Object.entries(tag)) el.setAttribute(k, v);
|
|
1419
|
-
keepManaged.add(el);
|
|
1420
|
-
}
|
|
1421
|
-
for (const el of [...document.head.querySelectorAll(`[${ILHA_HEAD_ATTR}]`)]) if (!keepManaged.has(el)) el.remove();
|
|
1422
|
-
const htmlEl = document.documentElement;
|
|
1423
|
-
const prevHtmlKeys = (htmlEl.getAttribute(ILHA_ROUTER_HTML_ATTR) ?? "").split(/\s+/).filter(Boolean);
|
|
1424
|
-
for (const k of prevHtmlKeys) htmlEl.removeAttribute(k);
|
|
1425
|
-
const nextHtmlKeys = Object.keys(htmlAttrs);
|
|
1426
|
-
for (const [k, v] of Object.entries(htmlAttrs)) htmlEl.setAttribute(k, v);
|
|
1427
|
-
if (nextHtmlKeys.length) htmlEl.setAttribute(ILHA_ROUTER_HTML_ATTR, nextHtmlKeys.join(" "));
|
|
1428
|
-
else htmlEl.removeAttribute(ILHA_ROUTER_HTML_ATTR);
|
|
1429
|
-
const bodyEl = document.body;
|
|
1430
|
-
const prevBodyKeys = (bodyEl.getAttribute(ILHA_ROUTER_BODY_ATTR) ?? "").split(/\s+/).filter(Boolean);
|
|
1431
|
-
for (const k of prevBodyKeys) bodyEl.removeAttribute(k);
|
|
1432
|
-
const nextBodyKeys = Object.keys(bodyAttrs);
|
|
1433
|
-
for (const [k, v] of Object.entries(bodyAttrs)) bodyEl.setAttribute(k, v);
|
|
1434
|
-
if (nextBodyKeys.length) bodyEl.setAttribute(ILHA_ROUTER_BODY_ATTR, nextBodyKeys.join(" "));
|
|
1435
|
-
else bodyEl.removeAttribute(ILHA_ROUTER_BODY_ATTR);
|
|
1436
|
-
}
|
|
1437
|
-
async function withHeadStore(store, fn) {
|
|
1438
|
-
if (isBrowser) {
|
|
1439
|
-
const prev = _browserHeadStore;
|
|
1440
|
-
_browserHeadStore = store;
|
|
1441
|
-
try {
|
|
1442
|
-
return await fn();
|
|
1443
|
-
} finally {
|
|
1444
|
-
_browserHeadStore = prev;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
return await (await getHeadAlsAsync()).run(store, () => Promise.resolve(fn()));
|
|
1448
|
-
}
|
|
1449
|
-
const HEAD_ESC = {
|
|
1450
|
-
"&": "&",
|
|
1451
|
-
"<": "<",
|
|
1452
|
-
">": ">",
|
|
1453
|
-
"\"": """,
|
|
1454
|
-
"'": "'"
|
|
1455
|
-
};
|
|
1456
|
-
function escapeHeadAttr(value) {
|
|
1457
|
-
return String(value).replace(/[&<>"']/g, (c) => HEAD_ESC[c]);
|
|
1458
|
-
}
|
|
1459
|
-
/** Escape text content for inline HTML (loader error messages etc.). */
|
|
1460
|
-
function escapeHtml(value) {
|
|
1461
|
-
return String(value).replace(/[&<>]/g, (c) => HEAD_ESC[c]);
|
|
1462
|
-
}
|
|
1463
|
-
function serializeAttrs(attrs) {
|
|
1464
|
-
const parts = [];
|
|
1465
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
1466
|
-
if (!/^[A-Za-z_:][A-Za-z0-9:._-]*$/.test(k) || /^on[a-z]/i.test(k)) {
|
|
1467
|
-
if (isDevEnv()) console.warn(`[ilha-router] Dropping unsafe head attribute "${k}".`);
|
|
1468
|
-
continue;
|
|
1469
|
-
}
|
|
1470
|
-
parts.push(` ${k}="${escapeHeadAttr(v)}"`);
|
|
1471
|
-
}
|
|
1472
|
-
return parts.join("");
|
|
1473
|
-
}
|
|
1474
|
-
function isSafeUrl(value) {
|
|
1475
|
-
const v = value.trim();
|
|
1476
|
-
if (v === "") return true;
|
|
1477
|
-
if (v.startsWith("//")) return false;
|
|
1478
|
-
if (v.startsWith("#") || v.startsWith("/") || v.startsWith("./")) return true;
|
|
1479
|
-
if (/^(?:javascript|vbscript|data):/i.test(v)) return false;
|
|
1480
|
-
try {
|
|
1481
|
-
const u = new URL(v, "http://localhost");
|
|
1482
|
-
return u.protocol === "http:" || u.protocol === "https:";
|
|
1483
|
-
} catch {
|
|
1484
|
-
return false;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
function metaDedupKey(tag) {
|
|
1488
|
-
if ("charset" in tag) return "charset";
|
|
1489
|
-
if ("name" in tag) return `name:${tag.name}`;
|
|
1490
|
-
if ("property" in tag) return `property:${tag.property}`;
|
|
1491
|
-
if ("http-equiv" in tag) return `http-equiv:${tag["http-equiv"]}`;
|
|
1492
|
-
return JSON.stringify(tag);
|
|
1493
|
-
}
|
|
1494
|
-
function dedupByKey(tags, keyOf) {
|
|
1495
|
-
const map = /* @__PURE__ */ new Map();
|
|
1496
|
-
for (const tag of tags) map.set(keyOf(tag), tag);
|
|
1497
|
-
return [...map.values()];
|
|
1498
|
-
}
|
|
1499
|
-
function applyTitleTemplate(title, template) {
|
|
1500
|
-
if (template === void 0) return title;
|
|
1501
|
-
if (typeof template === "function") return template(title);
|
|
1502
|
-
return template.replace(/%s/g, title ?? "");
|
|
1503
|
-
}
|
|
1504
|
-
/**
|
|
1505
|
-
* Merge head entries in contribution order (loader first as the base, then
|
|
1506
|
-
* render-time outer→inner layouts, then the page) and serialize. Later entries
|
|
1507
|
-
* win on collision; the last `titleTemplate` wraps the resolved title.
|
|
1508
|
-
*/
|
|
1509
|
-
function serializeHead(entries) {
|
|
1510
|
-
let title;
|
|
1511
|
-
let titleTemplate;
|
|
1512
|
-
const meta = [];
|
|
1513
|
-
const link = [];
|
|
1514
|
-
const script = [];
|
|
1515
|
-
let htmlAttrs = {};
|
|
1516
|
-
let bodyAttrs = {};
|
|
1517
|
-
for (const entry of entries) {
|
|
1518
|
-
if (entry.title !== void 0) title = entry.title;
|
|
1519
|
-
if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
|
|
1520
|
-
if (entry.meta) meta.push(...entry.meta);
|
|
1521
|
-
if (entry.link) link.push(...entry.link);
|
|
1522
|
-
if (entry.script) script.push(...entry.script);
|
|
1523
|
-
if (entry.htmlAttrs) htmlAttrs = {
|
|
1524
|
-
...htmlAttrs,
|
|
1525
|
-
...entry.htmlAttrs
|
|
1526
|
-
};
|
|
1527
|
-
if (entry.bodyAttrs) bodyAttrs = {
|
|
1528
|
-
...bodyAttrs,
|
|
1529
|
-
...entry.bodyAttrs
|
|
1530
|
-
};
|
|
1531
|
-
}
|
|
1532
|
-
const resolvedTitle = applyTitleTemplate(title, titleTemplate);
|
|
1533
|
-
const parts = [];
|
|
1534
|
-
if (resolvedTitle !== void 0) parts.push(`<title>${escapeHeadAttr(resolvedTitle)}</title>`);
|
|
1535
|
-
for (const tag of dedupByKey(meta, metaDedupKey)) {
|
|
1536
|
-
if (/^refresh$/i.test(tag["http-equiv"] ?? "") && !isSafeRefreshTarget(tag.content ?? "")) {
|
|
1537
|
-
if (isDevEnv()) console.warn(`[ilha-router] Dropping unsafe meta refresh target "${tag.content}".`);
|
|
1538
|
-
continue;
|
|
1539
|
-
}
|
|
1540
|
-
parts.push(`<meta${serializeAttrs({
|
|
1541
|
-
...tag,
|
|
1542
|
-
[ILHA_HEAD_ATTR]: ""
|
|
1543
|
-
})} />`);
|
|
1544
|
-
}
|
|
1545
|
-
for (const tag of dedupByKey(link, (t) => `${t.rel ?? ""}:${t.href ?? ""}`)) {
|
|
1546
|
-
if (tag.href !== void 0 && !isSafeUrl(tag.href)) {
|
|
1547
|
-
if (isDevEnv()) console.warn(`[ilha-router] Dropping unsafe link href "${tag.href}".`);
|
|
1548
|
-
continue;
|
|
1549
|
-
}
|
|
1550
|
-
parts.push(`<link${serializeAttrs({
|
|
1551
|
-
...tag,
|
|
1552
|
-
[ILHA_HEAD_ATTR]: ""
|
|
1553
|
-
})} />`);
|
|
1554
|
-
}
|
|
1555
|
-
for (const tag of script) {
|
|
1556
|
-
if (tag.src !== void 0 && !isSafeUrl(tag.src)) {
|
|
1557
|
-
if (isDevEnv()) console.warn(`[ilha-router] Dropping unsafe script src "${tag.src}".`);
|
|
1558
|
-
continue;
|
|
1559
|
-
}
|
|
1560
|
-
const { children, ...attrs } = tag;
|
|
1561
|
-
const body = (children ?? "").replace(/<\/script/gi, "<\\/script");
|
|
1562
|
-
parts.push(`<script${serializeAttrs(attrs)}>${body}<\/script>`);
|
|
1563
|
-
}
|
|
1564
|
-
return {
|
|
1565
|
-
headTags: parts.join("\n "),
|
|
1566
|
-
htmlAttrs: serializeAttrs(htmlAttrs),
|
|
1567
|
-
bodyAttrs: serializeAttrs(bodyAttrs)
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
function isSafeRefreshTarget(content) {
|
|
1571
|
-
const match = /url\s*=\s*(['"]?)([^'";\s]+)\1/i.exec(content);
|
|
1572
|
-
if (!match) return true;
|
|
1573
|
-
return isSafeUrl(match[2] ?? "");
|
|
1574
|
-
}
|
|
1575
|
-
/**
|
|
1576
|
-
* Build an HTTP `Response` for SSR output with sensible security headers:
|
|
1577
|
-
* `Content-Type: text/html`, `X-Content-Type-Options: nosniff`,
|
|
1578
|
-
* `Referrer-Policy`, `Cache-Control: no-store`, and an optional CSP. This is
|
|
1579
|
-
* a low-level helper — prefer {@link RouterBuilder.respond} for the full
|
|
1580
|
-
* render+head+headers pipeline.
|
|
1581
|
-
*/
|
|
1582
|
-
function httpResponse(body, options = {}) {
|
|
1583
|
-
const headers = new Headers(options.headers);
|
|
1584
|
-
if (body != null && !headers.has("content-type")) headers.set("content-type", "text/html; charset=utf-8");
|
|
1585
|
-
if (!headers.has("x-content-type-options")) headers.set("x-content-type-options", "nosniff");
|
|
1586
|
-
if (!headers.has("referrer-policy")) headers.set("referrer-policy", "no-referrer");
|
|
1587
|
-
if (!headers.has("cache-control")) headers.set("cache-control", "no-store");
|
|
1588
|
-
const csp = options.contentSecurityPolicy ?? (options.cspNonce ? `default-src 'self'; script-src 'self' 'nonce-${options.cspNonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'` : void 0);
|
|
1589
|
-
if (csp) headers.set("content-security-policy", csp);
|
|
1590
|
-
return new Response(body, {
|
|
1591
|
-
status: options.status ?? 200,
|
|
1592
|
-
headers
|
|
1593
|
-
});
|
|
1594
|
-
}
|
|
1595
|
-
const EMPTY_HEAD = {
|
|
1596
|
-
headTags: "",
|
|
1597
|
-
htmlAttrs: "",
|
|
1598
|
-
bodyAttrs: ""
|
|
1599
|
-
};
|
|
1600
1397
|
function resolveRequestUrl(urlOrRequest, request) {
|
|
1601
1398
|
if (urlOrRequest instanceof Request) return {
|
|
1602
1399
|
url: urlOrRequest.url,
|
|
@@ -1620,15 +1417,13 @@ function isDevEnv() {
|
|
|
1620
1417
|
}
|
|
1621
1418
|
/**
|
|
1622
1419
|
* Validate a loader redirect target. Relative paths always pass; same-origin
|
|
1623
|
-
* absolute URLs collapse to a path; cross-origin targets
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1420
|
+
* absolute URLs collapse to a path; cross-origin targets (including
|
|
1421
|
+
* protocol-relative `//host` URLs) are rejected unless `allowExternal` is set.
|
|
1422
|
+
* Non-http(s) schemes, backslash/control-char smuggling, and unparsable
|
|
1423
|
+
* targets are always rejected.
|
|
1626
1424
|
*/
|
|
1627
1425
|
function resolveRedirectTarget(to, base, allowExternal) {
|
|
1628
|
-
if (
|
|
1629
|
-
ok: true,
|
|
1630
|
-
to
|
|
1631
|
-
};
|
|
1426
|
+
if (/[\\\u0000-\u0020]/.test(to)) return { ok: false };
|
|
1632
1427
|
try {
|
|
1633
1428
|
const u = new URL(to, base);
|
|
1634
1429
|
if (!/^https?:$/.test(u.protocol)) return { ok: false };
|
|
@@ -1712,7 +1507,7 @@ async function executeLoader(loader, url, params, request, signal, onHead) {
|
|
|
1712
1507
|
console.error("[ilha-router] loader failed:", e);
|
|
1713
1508
|
return {
|
|
1714
1509
|
kind: "error",
|
|
1715
|
-
status: typeof e?.status === "number" ? e.status : 500,
|
|
1510
|
+
status: typeof e?.status === "number" && e.status >= 400 && e.status <= 599 ? e.status : 500,
|
|
1716
1511
|
message: isDevEnv() ? e?.message ?? "Loader failed" : "Internal error"
|
|
1717
1512
|
};
|
|
1718
1513
|
}
|
|
@@ -1865,7 +1660,7 @@ function router(options = {}) {
|
|
|
1865
1660
|
};
|
|
1866
1661
|
const reverseRegistry = registry ? buildReverseRegistry(registry) : void 0;
|
|
1867
1662
|
let navVersion = 0;
|
|
1868
|
-
const NavHandler = ilha
|
|
1663
|
+
const NavHandler = ilha(() => {
|
|
1869
1664
|
const current = activeIsland();
|
|
1870
1665
|
const pathWithSearch = routePath() + routeSearch();
|
|
1871
1666
|
if (current !== currentMountedIsland || pathWithSearch !== currentMountedPath) {
|
|
@@ -2042,7 +1837,7 @@ function router(options = {}) {
|
|
|
2042
1837
|
}
|
|
2043
1838
|
const escaped = escapeHtml(result.message);
|
|
2044
1839
|
await withViewSwap(() => {
|
|
2045
|
-
viewHost.innerHTML = `<div data-router-error="${result.status}">${escaped}</div>`;
|
|
1840
|
+
viewHost.innerHTML = `<div data-router-error="${escapeHtml(result.status)}">${escaped}</div>`;
|
|
2046
1841
|
});
|
|
2047
1842
|
return;
|
|
2048
1843
|
}
|
|
@@ -2066,7 +1861,7 @@ function router(options = {}) {
|
|
|
2066
1861
|
if (e?.name === "AbortError") return;
|
|
2067
1862
|
console.error("[ilha-router] initial mount failed:", e);
|
|
2068
1863
|
});
|
|
2069
|
-
const NavHandler = ilha
|
|
1864
|
+
const NavHandler = ilha(() => {
|
|
2070
1865
|
const current = activeIsland();
|
|
2071
1866
|
const pathWithSearch = routePath() + routeSearch();
|
|
2072
1867
|
if (current !== currentMountedIsland || pathWithSearch !== currentMountedPath) {
|
|
@@ -2155,7 +1950,8 @@ function router(options = {}) {
|
|
|
2155
1950
|
});
|
|
2156
1951
|
}
|
|
2157
1952
|
const head = res.head ?? EMPTY_HEAD;
|
|
2158
|
-
|
|
1953
|
+
const body = shell ? shell(head, res.html) : res.html;
|
|
1954
|
+
return httpResponse(body, {
|
|
2159
1955
|
status: status ?? res.status ?? 200,
|
|
2160
1956
|
headers,
|
|
2161
1957
|
cspNonce,
|
|
@@ -2296,14 +2092,14 @@ function router(options = {}) {
|
|
|
2296
2092
|
kind: "error",
|
|
2297
2093
|
status: result.status,
|
|
2298
2094
|
message: result.message,
|
|
2299
|
-
html: `<div data-router-view data-router-error="${result.status}">${html}</div>`,
|
|
2095
|
+
html: `<div data-router-view data-router-error="${escapeHtml(result.status)}">${html}</div>`,
|
|
2300
2096
|
head: serializeHead(headStore.entries)
|
|
2301
2097
|
};
|
|
2302
2098
|
} catch (e) {
|
|
2303
2099
|
console.error("[ilha-router] error boundary threw while rendering a loader error:", e);
|
|
2304
2100
|
}
|
|
2305
2101
|
const escapedMessage = escapeHtml(result.message);
|
|
2306
|
-
const html = `<div data-router-view data-router-error="${result.status}">${escapedMessage}</div>`;
|
|
2102
|
+
const html = `<div data-router-view data-router-error="${escapeHtml(result.status)}">${escapedMessage}</div>`;
|
|
2307
2103
|
return {
|
|
2308
2104
|
kind: "error",
|
|
2309
2105
|
status: result.status,
|
|
@@ -2362,4 +2158,4 @@ var src_default = {
|
|
|
2362
2158
|
};
|
|
2363
2159
|
|
|
2364
2160
|
//#endregion
|
|
2365
|
-
export {
|
|
2161
|
+
export { wrapLayout as A, routePath as C, useContext as D, src_default as E, getHistoryMode as F, setHistoryMode as I, parsePattern as M, safeDecode as N, useRoute as O, httpResponse as P, routeParams as S, router as T, prefetch as _, RouterView as a, resolveRedirectTarget as b, composeLoaders as c, error as d, invalidate as f, navigating as g, navigate as h, RouterLink as i, matchSegments as j, wrapError as k, defineLayout as l, loader as m, LoaderError as n, afterNavigate as o, isActive as p, Redirect as r, beforeNavigate as s, LOADER_ENDPOINT as t, enableLinkInterception as u, prime as v, routeSearch as w, routeHash as x, redirect as y };
|