@ilha/router 0.10.4 → 0.11.1

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/dist/index.js CHANGED
@@ -1,4 +1,701 @@
1
- import { A as wrapLayout, C as routePath, D as useContext, E as src_default, F as getHistoryMode, I as setHistoryMode, O as useRoute, P as httpResponse, S as routeParams, T as router, _ as prefetch, a as RouterView, b as resolveRedirectTarget, c as composeLoaders, d as error, f as invalidate, g as navigating, h as navigate, i as RouterLink, k as wrapError, l as defineLayout, m as loader, n as LoaderError, o as afterNavigate, p as isActive, r as Redirect, s as beforeNavigate, t as LOADER_ENDPOINT, u as enableLinkInterception, v as prime, w as routeSearch, x as routeHash, y as redirect } from "./src-PCXNZfql.js";
2
- import { o as head, s as serializeHead } from "./snapshot-CsEaY6h_.js";
1
+ import { t as REQUEST_ALS_KEY } from "./als-key-CVxbuM3z.js";
2
+ import { i as withHeadStore, n as head, r as serializeHead, t as applyHeadEntriesToDocument } from "./head-DpYV3bHH.js";
3
+ import { h, mount, renderToString } from "ilha";
3
4
 
4
- export { LOADER_ENDPOINT, LoaderError, Redirect, RouterLink, RouterView, afterNavigate, beforeNavigate, composeLoaders, src_default as default, defineLayout, enableLinkInterception, error, getHistoryMode, head, httpResponse, invalidate, isActive, loader, navigate, navigating, prefetch, prime, redirect, resolveRedirectTarget, routeHash, routeParams, routePath, routeSearch, router, serializeHead, setHistoryMode, useContext, useRoute, wrapError, wrapLayout };
5
+ //#region src/hash.ts
6
+ const isBrowser$1 = typeof window !== "undefined" && typeof document !== "undefined";
7
+ const historyAdapter = {
8
+ readLocation() {
9
+ if (!isBrowser$1) return {
10
+ pathname: "/",
11
+ search: "",
12
+ hash: ""
13
+ };
14
+ return {
15
+ pathname: location.pathname,
16
+ search: location.search,
17
+ hash: location.hash
18
+ };
19
+ },
20
+ push(to, state) {
21
+ if (!isBrowser$1) return;
22
+ history.pushState(state ?? null, "", to);
23
+ },
24
+ replace(to, state) {
25
+ if (!isBrowser$1) return;
26
+ history.replaceState(state ?? null, "", to);
27
+ },
28
+ onChange(handler) {
29
+ if (!isBrowser$1) return () => {};
30
+ window.addEventListener("popstate", handler);
31
+ return () => window.removeEventListener("popstate", handler);
32
+ },
33
+ toLinkHref(p) {
34
+ return p;
35
+ },
36
+ extractLogicalPath(anchor) {
37
+ const href = anchor.getAttribute("href");
38
+ if (!href) return null;
39
+ if (anchor.protocol && !/^(http:|https:)$/.test(anchor.protocol)) return null;
40
+ if (href.startsWith("#")) return null;
41
+ if (!!anchor.hostname && (anchor.hostname !== location.hostname || anchor.protocol !== location.protocol)) return null;
42
+ return anchor.pathname + anchor.search + anchor.hash;
43
+ }
44
+ };
45
+ function parseHash(rawHash) {
46
+ const stripped = rawHash.startsWith("#") ? rawHash.slice(1) : rawHash;
47
+ const path = stripped === "" ? "/" : stripped.startsWith("/") ? stripped : "/" + stripped;
48
+ const u = new URL(path, "http://_");
49
+ return {
50
+ pathname: u.pathname,
51
+ search: u.search,
52
+ hash: u.hash
53
+ };
54
+ }
55
+ const hashAdapter = {
56
+ readLocation() {
57
+ if (!isBrowser$1) return {
58
+ pathname: "/",
59
+ search: "",
60
+ hash: ""
61
+ };
62
+ return parseHash(location.hash);
63
+ },
64
+ push(to, state) {
65
+ if (!isBrowser$1) return;
66
+ history.pushState(state ?? null, "", to.startsWith("#") ? to : "#" + to);
67
+ },
68
+ replace(to, state) {
69
+ if (!isBrowser$1) return;
70
+ history.replaceState(state ?? null, "", to.startsWith("#") ? to : "#" + to);
71
+ },
72
+ onChange(handler) {
73
+ if (!isBrowser$1) return () => {};
74
+ window.addEventListener("popstate", handler);
75
+ window.addEventListener("hashchange", handler);
76
+ return () => {
77
+ window.removeEventListener("popstate", handler);
78
+ window.removeEventListener("hashchange", handler);
79
+ };
80
+ },
81
+ toLinkHref(p) {
82
+ if (p.startsWith("#")) return p;
83
+ return "#" + p;
84
+ },
85
+ extractLogicalPath(anchor) {
86
+ const href = anchor.getAttribute("href");
87
+ if (!href) return null;
88
+ if (href.startsWith("//")) return null;
89
+ if (anchor.protocol && !/^(http:|https:)$/.test(anchor.protocol)) return null;
90
+ if (href.startsWith("#")) {
91
+ const inner = href.slice(1);
92
+ if (inner === "" || !inner.startsWith("/")) return null;
93
+ return inner;
94
+ }
95
+ if (/^https?:\/\//i.test(href)) try {
96
+ const u = new URL(href);
97
+ if (u.origin !== location.origin) return null;
98
+ if (!u.hash || u.hash === "#") return null;
99
+ const inner = u.hash.slice(1);
100
+ if (!inner.startsWith("/")) return null;
101
+ return inner;
102
+ } catch {
103
+ return null;
104
+ }
105
+ return href;
106
+ }
107
+ };
108
+ let _mode = "history";
109
+ let _adapter = historyAdapter;
110
+ /**
111
+ * Set the router's history mode. Call this once at app entry, before
112
+ * mounting any router. Defaults to "history" (HTML5 History API).
113
+ *
114
+ * Use "hash" when the document is loaded over file:// (Electron, Tauri, etc.)
115
+ * or any time there's no server able to serve a SPA fallback at arbitrary
116
+ * pathnames.
117
+ *
118
+ * Switching modes mid-session is supported but not common — listeners
119
+ * registered before the switch will keep using their original adapter
120
+ * until they're re-attached (typically by unmounting and remounting
121
+ * the router).
122
+ */
123
+ function setHistoryMode(mode) {
124
+ _mode = mode;
125
+ _adapter = mode === "hash" ? hashAdapter : historyAdapter;
126
+ }
127
+ function getHistoryMode() {
128
+ return _mode;
129
+ }
130
+ /** Internal — used by index.ts. Not part of the public API. */
131
+ function getAdapter() {
132
+ return _adapter;
133
+ }
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
+
166
+ //#endregion
167
+ //#region src/route-match.ts
168
+ function parsePattern(pattern) {
169
+ const segments = pattern.split("/").filter(Boolean);
170
+ return {
171
+ segments,
172
+ kinds: segments.map((segment) => segment.startsWith("*") ? 0 : segment.startsWith(":") ? 1 : 2)
173
+ };
174
+ }
175
+ /**
176
+ * Match `segments` against a pathname. Returns raw (still-encoded) captured
177
+ * params, or `null` when the path doesn't match — the caller decides whether to
178
+ * decode via {@link safeDecode}.
179
+ */
180
+ function matchSegments(segments, pathname) {
181
+ const pathSegments = pathname.split("/").filter(Boolean);
182
+ const params = {};
183
+ let cursor = 0;
184
+ for (let i = 0; i < segments.length; i++) {
185
+ const segment = segments[i];
186
+ const isLast = i === segments.length - 1;
187
+ if (segment.startsWith("**") && isLast) {
188
+ const name = segment.slice(2).replace(/^:/, "");
189
+ if (name) params[name] = pathSegments.slice(cursor).join("/");
190
+ cursor = pathSegments.length;
191
+ break;
192
+ }
193
+ if (segment.startsWith("*")) {
194
+ if (pathSegments[cursor] === void 0) return null;
195
+ cursor++;
196
+ continue;
197
+ }
198
+ if (segment.startsWith(":")) {
199
+ const value = pathSegments[cursor];
200
+ if (value === void 0) return null;
201
+ params[segment.slice(1)] = value;
202
+ cursor++;
203
+ } else if (pathSegments[cursor] === segment) cursor++;
204
+ else return null;
205
+ }
206
+ return cursor === pathSegments.length ? params : null;
207
+ }
208
+ /** Decode a route-param value, tolerating malformed percent-encoding. */
209
+ function safeDecode(value) {
210
+ try {
211
+ return decodeURIComponent(value);
212
+ } catch {
213
+ return value;
214
+ }
215
+ }
216
+
217
+ //#endregion
218
+ //#region src/index.ts
219
+ const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
220
+ var Redirect = class {
221
+ __ilhaRedirect = true;
222
+ to;
223
+ status;
224
+ constructor(to, status = 302) {
225
+ this.to = to;
226
+ this.status = status >= 300 && status <= 399 ? status : 302;
227
+ }
228
+ };
229
+ var RouteError = class {
230
+ __ilhaRouteError = true;
231
+ status;
232
+ message;
233
+ constructor(status, message) {
234
+ this.message = message;
235
+ this.status = status >= 400 && status <= 599 ? status : 500;
236
+ }
237
+ };
238
+ function redirect(to, status = 302) {
239
+ throw new Redirect(to, status);
240
+ }
241
+ function error(status, message) {
242
+ throw new RouteError(status, message);
243
+ }
244
+ function wrapLayout(layout, page) {
245
+ return function Wrapped() {
246
+ return h(layout, null, h(page, null));
247
+ };
248
+ }
249
+ function wrapError(handler, page) {
250
+ return async function Wrapped() {
251
+ try {
252
+ const out = page();
253
+ return out instanceof Promise ? await out : out;
254
+ } catch (e) {
255
+ const view = handler(e instanceof RouteError ? {
256
+ message: e.message,
257
+ status: e.status
258
+ } : { message: e instanceof Error ? e.message : String(e) }, snapshot());
259
+ return typeof view === "function" ? view() : view;
260
+ }
261
+ };
262
+ }
263
+ function defineLayout(layout) {
264
+ return layout;
265
+ }
266
+ function createRouteRegistry() {
267
+ return [];
268
+ }
269
+ function addRouteEntry(registry, pattern, data) {
270
+ registry.push({
271
+ pattern,
272
+ parsed: parsePattern(pattern),
273
+ data
274
+ });
275
+ }
276
+ function matchRoute(registry, pathname) {
277
+ let best = null;
278
+ let bestScore = -1;
279
+ for (const entry of registry) {
280
+ if (matchSegments(entry.parsed.segments, pathname) == null) continue;
281
+ const score = entry.parsed.kinds.reduce((s, k) => s + k, 0);
282
+ if (score > bestScore) {
283
+ best = entry;
284
+ bestScore = score;
285
+ }
286
+ }
287
+ return best;
288
+ }
289
+ function extractParams(match, pathname) {
290
+ if (!match) return {};
291
+ const raw = matchSegments(match.parsed.segments, pathname) ?? {};
292
+ const out = {};
293
+ for (const [k, v] of Object.entries(raw)) out[k] = safeDecode(v);
294
+ return out;
295
+ }
296
+ let _routes = [];
297
+ let _path = "/";
298
+ let _params = {};
299
+ let _search = "";
300
+ let _hash = "";
301
+ const _navigating = false;
302
+ let _navKeyCounter = 0;
303
+ let _lastNavKey = 0;
304
+ const _scrollPositions = /* @__PURE__ */ new Map();
305
+ const _beforeNavigateHooks = [];
306
+ const _afterNavigateHooks = [];
307
+ function snapshot() {
308
+ return {
309
+ path: _path,
310
+ params: _params,
311
+ search: _search,
312
+ hash: _hash
313
+ };
314
+ }
315
+ function routePath() {
316
+ return _path;
317
+ }
318
+ function routeParams() {
319
+ return _params;
320
+ }
321
+ function routeSearch() {
322
+ return _search;
323
+ }
324
+ function routeHash() {
325
+ return _hash;
326
+ }
327
+ function navigating() {
328
+ return _navigating;
329
+ }
330
+ function useRoute() {
331
+ return {
332
+ path: routePath,
333
+ params: routeParams,
334
+ search: routeSearch,
335
+ hash: routeHash,
336
+ navigating
337
+ };
338
+ }
339
+ function useContext() {
340
+ return { request: globalThis[REQUEST_ALS_KEY]?.getStore?.() };
341
+ }
342
+ function syncFromParts(pathname, search, hash, routes = _routes) {
343
+ _path = pathname || "/";
344
+ _search = search;
345
+ _hash = hash;
346
+ _params = extractParams(matchRoute(routes, _path), _path);
347
+ }
348
+ function syncRouteFromLocation() {
349
+ const loc = getAdapter().readLocation();
350
+ syncFromParts(loc.pathname, loc.search, loc.hash);
351
+ }
352
+ function prime() {
353
+ if (isBrowser) syncRouteFromLocation();
354
+ }
355
+ function beforeNavigate(fn) {
356
+ _beforeNavigateHooks.push(fn);
357
+ return () => {
358
+ const i = _beforeNavigateHooks.indexOf(fn);
359
+ if (i >= 0) _beforeNavigateHooks.splice(i, 1);
360
+ };
361
+ }
362
+ function afterNavigate(fn) {
363
+ _afterNavigateHooks.push(fn);
364
+ return () => {
365
+ const i = _afterNavigateHooks.indexOf(fn);
366
+ if (i >= 0) _afterNavigateHooks.splice(i, 1);
367
+ };
368
+ }
369
+ function runAfterNavigateHooks(nav) {
370
+ for (const fn of _afterNavigateHooks) try {
371
+ fn(nav);
372
+ } catch (e) {
373
+ console.error("[ilha-router] afterNavigate hook threw:", e);
374
+ }
375
+ }
376
+ function currentNavKey() {
377
+ const state = isBrowser ? history.state : null;
378
+ const key = state && typeof state === "object" ? state.__ilhaNavKey : void 0;
379
+ return typeof key === "number" ? key : 0;
380
+ }
381
+ function saveScrollPosition() {
382
+ if (!isBrowser) return;
383
+ _scrollPositions.set(_lastNavKey, {
384
+ x: window.scrollX,
385
+ y: window.scrollY
386
+ });
387
+ }
388
+ function scrollAfterNavigate(hash) {
389
+ if (!isBrowser) return;
390
+ if (hash && hash.length > 1) {
391
+ const id = hash.slice(1);
392
+ const el = document.getElementById(id);
393
+ if (el) {
394
+ el.scrollIntoView();
395
+ return;
396
+ }
397
+ }
398
+ window.scrollTo(0, 0);
399
+ }
400
+ function restoreScrollPosition() {
401
+ const pos = _scrollPositions.get(currentNavKey());
402
+ if (!pos) return;
403
+ requestAnimationFrame(() => window.scrollTo(pos.x, pos.y));
404
+ }
405
+ function navigate(to, opts = {}) {
406
+ if (!isBrowser) return;
407
+ const adapter = getAdapter();
408
+ const cur = adapter.readLocation();
409
+ const current = cur.pathname + cur.search + cur.hash;
410
+ if (to === current) return;
411
+ const type = opts.replace ? "replace" : "push";
412
+ let cancelled = false;
413
+ for (const fn of _beforeNavigateHooks) try {
414
+ fn({
415
+ from: current,
416
+ to,
417
+ type,
418
+ cancel: () => cancelled = true
419
+ });
420
+ } catch (e) {
421
+ console.error("[ilha-router] beforeNavigate hook threw:", e);
422
+ }
423
+ if (cancelled) return;
424
+ if (opts.replace) adapter.replace(to, { __ilhaNavKey: currentNavKey() });
425
+ else {
426
+ saveScrollPosition();
427
+ _navKeyCounter = Math.max(_navKeyCounter + 1, currentNavKey() + 1);
428
+ adapter.push(to, { __ilhaNavKey: _navKeyCounter });
429
+ }
430
+ _lastNavKey = currentNavKey();
431
+ const prevPath = cur.pathname;
432
+ syncRouteFromLocation();
433
+ if (opts.scroll !== false) {
434
+ const dest = adapter.readLocation();
435
+ const fromMatch = matchRoute(_routes, prevPath);
436
+ const destMatch = matchRoute(_routes, dest.pathname);
437
+ if (!(fromMatch?.data.page != null && destMatch?.data.page === fromMatch.data.page) || dest.hash && dest.hash !== "#") scrollAfterNavigate(dest.hash);
438
+ }
439
+ runAfterNavigateHooks({
440
+ from: current,
441
+ to,
442
+ type
443
+ });
444
+ }
445
+ function enableLinkInterception(root = document, _options = {}) {
446
+ if (!isBrowser) return () => {};
447
+ const clickHandler = (event) => {
448
+ const e = event;
449
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
450
+ const a = e.target?.closest?.("a");
451
+ if (!a || a.hasAttribute("download") || a.getAttribute("target") === "_blank" || a.hasAttribute("data-no-intercept")) return;
452
+ const href = a.getAttribute("href");
453
+ if (!href || /^(mailto|tel|javascript):/i.test(href)) return;
454
+ if (href.startsWith("#") && getHistoryMode() !== "hash") return;
455
+ if (href.startsWith("#") && !href.startsWith("#/")) return;
456
+ let url;
457
+ try {
458
+ url = new URL(href, location.href);
459
+ } catch {
460
+ return;
461
+ }
462
+ if (url.origin !== location.origin) return;
463
+ e.preventDefault();
464
+ navigate(getHistoryMode() === "hash" ? url.hash.startsWith("#/") ? url.hash.slice(1) : url.pathname + url.search : url.pathname + url.search + url.hash);
465
+ };
466
+ root.addEventListener("click", clickHandler);
467
+ return () => root.removeEventListener("click", clickHandler);
468
+ }
469
+ function isActive(pattern, options = {}) {
470
+ if (options.end) return _path === pattern;
471
+ if (pattern === "/") return _path === "/";
472
+ return _path === pattern || _path.startsWith(pattern.endsWith("/") ? pattern : pattern + "/");
473
+ }
474
+ function resolveRedirectTarget(to, base, allowExternal) {
475
+ if (/[\\\u0000-\u0020]/.test(to)) return { ok: false };
476
+ try {
477
+ const u = new URL(to, base);
478
+ if (!/^https?:$/.test(u.protocol)) return { ok: false };
479
+ if (u.origin === base.origin) return {
480
+ ok: true,
481
+ to: u.pathname + u.search + u.hash
482
+ };
483
+ return allowExternal ? {
484
+ ok: true,
485
+ to: u.href
486
+ } : { ok: false };
487
+ } catch {
488
+ return { ok: false };
489
+ }
490
+ }
491
+ function resolveRequestUrl(urlOrRequest) {
492
+ try {
493
+ if (urlOrRequest instanceof Request) return new URL(urlOrRequest.url);
494
+ return typeof urlOrRequest === "string" ? new URL(urlOrRequest, "http://localhost") : urlOrRequest;
495
+ } catch {
496
+ return new URL("http://localhost/");
497
+ }
498
+ }
499
+ function pageForPath(pathname, routes, notFound) {
500
+ const match = matchRoute(routes, pathname);
501
+ if (match) return match.data.page;
502
+ return notFound;
503
+ }
504
+ async function renderPage(page, opts) {
505
+ return renderToString(page, opts);
506
+ }
507
+ function router(options = {}) {
508
+ const mode = options.mode ?? "spa";
509
+ const defaultInterceptLinks = options.interceptLinks !== false;
510
+ const allowExternalRedirects = options.allowExternalRedirects === true;
511
+ const records = [];
512
+ const routes = createRouteRegistry();
513
+ const patternToData = /* @__PURE__ */ new Map();
514
+ const notFound = options.notFound ?? null;
515
+ _routes = routes;
516
+ options.viewTransitions;
517
+ let navChangeCleanup = null;
518
+ let linkCleanup = null;
519
+ const builder = {
520
+ route(pattern, page) {
521
+ const data = {
522
+ page,
523
+ pattern
524
+ };
525
+ records.push({
526
+ pattern,
527
+ page
528
+ });
529
+ addRouteEntry(routes, pattern, data);
530
+ patternToData.set(pattern, data);
531
+ return builder;
532
+ },
533
+ errorBoundary(pattern, handler) {
534
+ const data = patternToData.get(pattern);
535
+ if (!data) {
536
+ console.warn(`[ilha-router] errorBoundary("${pattern}"): pattern was never registered via .route().`);
537
+ return builder;
538
+ }
539
+ data.errorHandler = handler;
540
+ return builder;
541
+ },
542
+ routes() {
543
+ return records.map((r) => ({ ...r }));
544
+ },
545
+ prime,
546
+ mount(target, { hydrate = false, interceptLinks: mountInterceptLinks } = {}) {
547
+ if (!isBrowser) {
548
+ console.warn("[ilha-router] mount() called in a non-browser environment");
549
+ return () => {};
550
+ }
551
+ const host = typeof target === "string" ? document.querySelector(target) : target;
552
+ if (!host) {
553
+ console.warn(`[ilha-router] No element found for selector "${target}"`);
554
+ return () => {};
555
+ }
556
+ if (mode === "static") {
557
+ console.warn("[ilha-router] router.mount() is a no-op in static mode.");
558
+ return () => {};
559
+ }
560
+ syncRouteFromLocation();
561
+ _lastNavKey = currentNavKey();
562
+ if (hydrate && getHistoryMode() === "hash") console.warn("[ilha-router] mount({ hydrate: true }) was called in hash mode. The server never sees the hash, so SSR HTML is for '/'.");
563
+ let mounted = true;
564
+ const prevScroll = "scrollRestoration" in history ? history.scrollRestoration : null;
565
+ if (prevScroll !== null) history.scrollRestoration = "manual";
566
+ const popHandler = () => {
567
+ if (!mounted) return;
568
+ const prevPath = _path + _search + _hash;
569
+ _scrollPositions.set(_lastNavKey, {
570
+ x: window.scrollX,
571
+ y: window.scrollY
572
+ });
573
+ _lastNavKey = currentNavKey();
574
+ syncRouteFromLocation();
575
+ restoreScrollPosition();
576
+ runAfterNavigateHooks({
577
+ from: prevPath,
578
+ to: _path + _search + _hash,
579
+ type: "pop"
580
+ });
581
+ remount();
582
+ };
583
+ navChangeCleanup = getAdapter().onChange(popHandler);
584
+ linkCleanup = mountInterceptLinks ?? defaultInterceptLinks ? enableLinkInterception(document) : null;
585
+ let unmountView = null;
586
+ const remount = () => {
587
+ unmountView?.();
588
+ const page = pageForPath(_path, routes, notFound);
589
+ if (!page) {
590
+ unmountView = null;
591
+ return;
592
+ }
593
+ unmountView = mount(host, page, { hydrate: unmountView == null && hydrate });
594
+ };
595
+ remount();
596
+ const offNav = afterNavigate(() => {
597
+ if (mounted) remount();
598
+ });
599
+ return () => {
600
+ mounted = false;
601
+ offNav();
602
+ unmountView?.();
603
+ navChangeCleanup?.();
604
+ linkCleanup?.();
605
+ if (prevScroll !== null) history.scrollRestoration = prevScroll;
606
+ };
607
+ },
608
+ async render(url, options) {
609
+ const result = await builder.renderResponse(url, options);
610
+ if (result.kind === "html") return result.html;
611
+ if (result.kind === "redirect") return "";
612
+ return result.html;
613
+ },
614
+ async renderResponse(url, options) {
615
+ const parsed = resolveRequestUrl(url);
616
+ syncFromParts(parsed.pathname, parsed.search, parsed.hash, routes);
617
+ const store = { entries: [] };
618
+ try {
619
+ const page = pageForPath(parsed.pathname, routes, notFound);
620
+ if (!page) throw new RouteError(404, "Not found");
621
+ let captured;
622
+ const wrapped = async () => {
623
+ try {
624
+ return await Promise.resolve(page());
625
+ } catch (err) {
626
+ captured = err;
627
+ throw err;
628
+ }
629
+ };
630
+ const html = await withHeadStore(store, () => renderPage(wrapped, options));
631
+ if (captured instanceof Redirect || captured instanceof RouteError) throw captured;
632
+ return {
633
+ kind: "html",
634
+ html,
635
+ status: 200,
636
+ head: serializeHead(store.entries)
637
+ };
638
+ } catch (e) {
639
+ if (e instanceof Redirect) {
640
+ const safe = resolveRedirectTarget(e.to, parsed, allowExternalRedirects);
641
+ if (!safe.ok) return {
642
+ kind: "error",
643
+ status: 500,
644
+ message: "unsafe redirect target",
645
+ html: "",
646
+ head: EMPTY_HEAD
647
+ };
648
+ return {
649
+ kind: "redirect",
650
+ to: safe.to,
651
+ status: e.status
652
+ };
653
+ }
654
+ if (e instanceof RouteError) {
655
+ const boundary = matchRoute(routes, parsed.pathname)?.data.errorHandler;
656
+ let html = "";
657
+ if (boundary) {
658
+ const view = boundary({
659
+ message: e.message,
660
+ status: e.status
661
+ }, snapshot());
662
+ html = await renderPage(typeof view === "function" ? view : async () => view, options);
663
+ }
664
+ return {
665
+ kind: "error",
666
+ status: e.status,
667
+ message: e.message,
668
+ html,
669
+ head: serializeHead(store.entries)
670
+ };
671
+ }
672
+ throw e;
673
+ }
674
+ },
675
+ async respond(url, options = {}) {
676
+ const result = await builder.renderResponse(url, options);
677
+ if (result.kind === "redirect") return Response.redirect(result.to, result.status);
678
+ const head = result.head ?? EMPTY_HEAD;
679
+ if (isBrowser && result.kind === "html") applyHeadEntriesToDocument([]);
680
+ const body = options.shell ? options.shell(head, result.kind === "html" ? result.html : result.html) : result.kind === "html" ? result.html : result.html;
681
+ return httpResponse(body, {
682
+ status: result.kind === "error" ? result.status : result.status ?? 200,
683
+ headers: options.headers,
684
+ cspNonce: options.cspNonce,
685
+ contentSecurityPolicy: options.contentSecurityPolicy
686
+ });
687
+ },
688
+ hydrate(opts = {}) {
689
+ const root = opts.root ?? (isBrowser ? document.body : null);
690
+ if (!root) return () => {};
691
+ return builder.mount(root, {
692
+ hydrate: true,
693
+ interceptLinks: opts.interceptLinks
694
+ });
695
+ }
696
+ };
697
+ return builder;
698
+ }
699
+
700
+ //#endregion
701
+ export { EMPTY_HEAD, Redirect, RouteError, afterNavigate, beforeNavigate, defineLayout, enableLinkInterception, error, getHistoryMode, head, httpResponse, isActive, navigate, navigating, prime, redirect, resolveRedirectTarget, routeHash, routeParams, routePath, routeSearch, router, serializeHead, setHistoryMode, useContext, useRoute, wrapError, wrapLayout };