@pracht/core 0.8.1 → 0.9.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 +5 -1
- package/dist/app-BN3pjxtw.d.mts +24 -0
- package/dist/{app-w-P1wf5T.mjs → app-CGRKjZ6Z.mjs} +96 -7
- package/dist/app-graph-BlaCcGUZ.mjs +52 -0
- package/dist/app-graph-DBoDsHJ5.d.mts +38 -0
- package/dist/browser.d.mts +7 -6
- package/dist/browser.mjs +8 -7
- package/dist/client.d.mts +3 -3
- package/dist/client.mjs +3 -3
- package/dist/dev-404.d.mts +26 -0
- package/dist/dev-404.mjs +166 -0
- package/dist/devtools.d.mts +8 -0
- package/dist/devtools.mjs +156 -0
- package/dist/error-overlay.d.mts +38 -1
- package/dist/error-overlay.mjs +134 -5
- package/dist/index.d.mts +9 -7
- package/dist/index.mjs +9 -7
- package/dist/manifest.d.mts +2 -1
- package/dist/manifest.mjs +1 -1
- package/dist/{runtime-context-B5pREhcM.mjs → navigation-state-BjwXTeVz.mjs} +74 -2
- package/dist/{prefetch-D9amIQtw.mjs → prefetch-COYeuvQK.mjs} +44 -27
- package/dist/prefetch-api-Bf-P7XFo.d.mts +39 -0
- package/dist/prefetch-api-ChSCTbEB.mjs +69 -0
- package/dist/{prefetch-cache-DzP2Bj9H.mjs → prefetch-cache-BNLEp9H5.mjs} +12 -1
- package/dist/{prerender-B8_CqYwd.d.mts → prerender-BV42B9jK.d.mts} +31 -2
- package/dist/{prerender-CoMyuJKm.mjs → prerender-C8BpANz3.mjs} +53 -9
- package/dist/{router-BcUmg1kB.d.mts → router-B_Aj5TtY.d.mts} +2 -2
- package/dist/{router-BkkyNjqB.mjs → router-E5Wh-dOR.mjs} +251 -58
- package/dist/{runtime-context-ytVd7GmZ.d.mts → runtime-context-BYBwVA0M.d.mts} +1 -1
- package/dist/{runtime-middleware-aeBdgjYr.d.mts → runtime-middleware-pYubngnL.d.mts} +57 -4
- package/dist/server.d.mts +7 -5
- package/dist/server.mjs +6 -5
- package/dist/{app-BWriseLj.d.mts → types-B-sfVjaH.d.mts} +42 -22
- package/dist/{types-DQv2poC5.mjs → types-DEPXdbyp.mjs} +43 -15
- package/package.json +9 -1
- package/dist/hydration-M1QzbQ1O.d.mts +0 -23
- /package/dist/{forwardRef-grZ6t4GS.mjs → forwardRef-B0cSkHwH.mjs} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { J as RouteId, at as RouteTarget } from "./types-B-sfVjaH.mjs";
|
|
2
|
+
import { FunctionComponent } from "preact";
|
|
3
|
+
|
|
4
|
+
//#region src/forwardRef.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Pass ref down to a child. This is mainly used in libraries with HOCs that
|
|
7
|
+
* wrap components. Using `forwardRef` there is an easy way to get a reference
|
|
8
|
+
* of the wrapped component instead of one of the wrapper itself.
|
|
9
|
+
*/
|
|
10
|
+
declare function forwardRef<P = {}>(fn: ((props: P, ref: any) => any) & {
|
|
11
|
+
displayName?: string;
|
|
12
|
+
}): FunctionComponent<P & {
|
|
13
|
+
ref?: any;
|
|
14
|
+
}>;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/hydration.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* Returns `true` once the initial hydration (including all Suspense
|
|
19
|
+
* boundaries) has fully resolved. During SSR and hydration this returns
|
|
20
|
+
* `false`.
|
|
21
|
+
*/
|
|
22
|
+
declare function useIsHydrated(): boolean;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/prefetch-api.d.ts
|
|
25
|
+
interface PrefetchFn {
|
|
26
|
+
(to: string): Promise<void>;
|
|
27
|
+
<TRoute extends RouteId>(to: RouteTarget<TRoute>): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Imperatively prefetch a route: warms the route/shell module chunks and
|
|
31
|
+
* caches the route-state JSON in the shared prefetch cache. Accepts an href
|
|
32
|
+
* string or a typed route target (`{ route, params, search }`).
|
|
33
|
+
*
|
|
34
|
+
* Available once the client router has initialized; a no-op during SSR,
|
|
35
|
+
* before hydration, and for URLs that do not match a client route.
|
|
36
|
+
*/
|
|
37
|
+
declare const prefetch: PrefetchFn;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { forwardRef as i, prefetch as n, useIsHydrated as r, PrefetchFn as t };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { o as matchAppRoute, t as buildHref } from "./app-CGRKjZ6Z.mjs";
|
|
2
|
+
import { a as removeCachedRouteState, c as fetchPrachtRouteState, d as routeNeedsServerFetch, i as getCachedRouteState, n as cacheRouteState, t as EMPTY_ROUTE_STATE_PROMISE } from "./prefetch-cache-BNLEp9H5.mjs";
|
|
3
|
+
//#region src/prefetch-api.ts
|
|
4
|
+
/**
|
|
5
|
+
* Imperative prefetch surface shared by the lazy prefetch listeners
|
|
6
|
+
* (`prefetch.ts`), the client router, and userland code via the public
|
|
7
|
+
* `prefetch()` export. Kept separate from `prefetch.ts` so importing the
|
|
8
|
+
* public API does not pull the document-level listener setup into the
|
|
9
|
+
* critical hydration path — every module imported here is already part of
|
|
10
|
+
* the core client bundle.
|
|
11
|
+
*/
|
|
12
|
+
let activePrefetchTarget = null;
|
|
13
|
+
/**
|
|
14
|
+
* Called by the client router during initialization so prefetching can match
|
|
15
|
+
* URLs against the resolved app and warm route/shell module chunks.
|
|
16
|
+
*/
|
|
17
|
+
function registerPrefetchTarget(app, warmModules) {
|
|
18
|
+
activePrefetchTarget = {
|
|
19
|
+
app,
|
|
20
|
+
warmModules
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch (or reuse) the route-state JSON for `url` and store it in the shared
|
|
25
|
+
* bounded prefetch cache so a subsequent client navigation can consume it
|
|
26
|
+
* without a second network request. Rejected fetches are evicted from the
|
|
27
|
+
* cache so a transient network error does not poison later navigations.
|
|
28
|
+
*/
|
|
29
|
+
function prefetchRouteState(url, route) {
|
|
30
|
+
if (route && !routeNeedsServerFetch(route)) return EMPTY_ROUTE_STATE_PROMISE;
|
|
31
|
+
const cached = getCachedRouteState(url);
|
|
32
|
+
if (cached) return cached;
|
|
33
|
+
const promise = fetchPrachtRouteState(url);
|
|
34
|
+
cacheRouteState(url, promise);
|
|
35
|
+
promise.catch(() => removeCachedRouteState(url, promise));
|
|
36
|
+
return promise;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Imperatively prefetch a route: warms the route/shell module chunks and
|
|
40
|
+
* caches the route-state JSON in the shared prefetch cache. Accepts an href
|
|
41
|
+
* string or a typed route target (`{ route, params, search }`).
|
|
42
|
+
*
|
|
43
|
+
* Available once the client router has initialized; a no-op during SSR,
|
|
44
|
+
* before hydration, and for URLs that do not match a client route.
|
|
45
|
+
*/
|
|
46
|
+
const prefetch = async (to) => {
|
|
47
|
+
if (typeof window === "undefined") return;
|
|
48
|
+
const target = activePrefetchTarget;
|
|
49
|
+
if (!target) return;
|
|
50
|
+
let href;
|
|
51
|
+
try {
|
|
52
|
+
href = typeof to === "string" ? to : buildHref(target.app.routes, to.route, to);
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
url = new URL(href, window.location.href);
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (url.origin !== window.location.origin) return;
|
|
63
|
+
const match = matchAppRoute(target.app, url.pathname);
|
|
64
|
+
if (!match) return;
|
|
65
|
+
target.warmModules?.(match);
|
|
66
|
+
await prefetchRouteState(url.pathname + url.search, match.route).catch(() => {});
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
export { prefetchRouteState as n, registerPrefetchTarget as r, prefetch as t };
|
|
@@ -4,6 +4,9 @@ const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
|
|
|
4
4
|
const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
|
|
5
5
|
const ROUTE_STATE_CACHE_CONTROL = "no-store";
|
|
6
6
|
const EMPTY_ROUTE_PARAMS = {};
|
|
7
|
+
const PREFETCH_ATTRIBUTE = "data-pracht-prefetch";
|
|
8
|
+
const PRESERVE_SCROLL_ATTRIBUTE = "data-pracht-preserve-scroll";
|
|
9
|
+
const VIEW_TRANSITION_ATTRIBUTE = "data-pracht-view-transition";
|
|
7
10
|
//#endregion
|
|
8
11
|
//#region src/runtime-client-fetch.ts
|
|
9
12
|
const SAFE_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
|
|
@@ -115,6 +118,14 @@ function cacheRouteState(url, promise) {
|
|
|
115
118
|
});
|
|
116
119
|
trimMapToSize(prefetchCache, MAX_PREFETCH_CACHE_ENTRIES);
|
|
117
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Remove a cached entry, but only when it still holds `promise` — a newer
|
|
123
|
+
* entry cached under the same URL must not be evicted by an older rejection.
|
|
124
|
+
*/
|
|
125
|
+
function removeCachedRouteState(url, promise) {
|
|
126
|
+
const entry = prefetchCache.get(url);
|
|
127
|
+
if (entry && entry.promise === promise) prefetchCache.delete(url);
|
|
128
|
+
}
|
|
118
129
|
function sweepPrefetchCache(now = Date.now()) {
|
|
119
130
|
for (const [url, entry] of prefetchCache) if (now - entry.timestamp > CACHE_TTL_MS) prefetchCache.delete(url);
|
|
120
131
|
}
|
|
@@ -126,4 +137,4 @@ function trimMapToSize(map, maxEntries) {
|
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
139
|
//#endregion
|
|
129
|
-
export {
|
|
140
|
+
export { ROUTE_STATE_REQUEST_HEADER as _, removeCachedRouteState as a, fetchPrachtRouteState as c, routeNeedsServerFetch as d, EMPTY_ROUTE_PARAMS as f, ROUTE_STATE_CACHE_CONTROL as g, PRESERVE_SCROLL_ATTRIBUTE as h, getCachedRouteState as i, navigateToClientLocation as l, PREFETCH_ATTRIBUTE as m, cacheRouteState as n, trimMapToSize as o, HYDRATION_STATE_ELEMENT_ID as p, clearPrefetchCache as r, buildRouteStateUrl as s, EMPTY_ROUTE_STATE_PROMISE as t, parseSafeNavigationUrl as u, SAFE_METHODS as v, VIEW_TRANSITION_ATTRIBUTE as y };
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as PrachtApp, N as ModuleRegistry, V as ResolvedApiRoute, nt as RouteRevalidate } from "./types-B-sfVjaH.mjs";
|
|
2
|
+
//#region src/runtime-timing.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Per-request phase timing for dev tooling.
|
|
5
|
+
*
|
|
6
|
+
* The runtime only records durations when a collector object is passed via
|
|
7
|
+
* `HandlePrachtRequestOptions.timings` — the dev server passes one, production
|
|
8
|
+
* adapters never do, so production requests skip all timing work.
|
|
9
|
+
*/
|
|
10
|
+
interface PrachtPhaseTimings {
|
|
11
|
+
/** Milliseconds spent in the middleware chain, excluding loader and render. */
|
|
12
|
+
mw?: number;
|
|
13
|
+
/** Milliseconds spent awaiting the route loader. */
|
|
14
|
+
loader?: number;
|
|
15
|
+
/** Milliseconds spent resolving modules and rendering the response, excluding the loader. */
|
|
16
|
+
render?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Format collected phase timings as a standards-compliant `Server-Timing`
|
|
20
|
+
* header value, e.g. `mw;dur=1.2, loader;dur=14.8, render;dur=3.1`.
|
|
21
|
+
* Returns an empty string when nothing was recorded.
|
|
22
|
+
*/
|
|
23
|
+
declare function formatServerTimingHeader(timings: PrachtPhaseTimings): string;
|
|
24
|
+
//#endregion
|
|
2
25
|
//#region src/runtime-headers.d.ts
|
|
3
26
|
declare function applyDefaultSecurityHeaders(headers: Headers): Headers;
|
|
4
27
|
//#endregion
|
|
@@ -16,6 +39,12 @@ interface HandlePrachtRequestOptions<TContext = unknown> {
|
|
|
16
39
|
/** Per-source-file JS chunk map produced by the vite plugin for modulepreload hints. */
|
|
17
40
|
jsManifest?: Record<string, string[]>;
|
|
18
41
|
apiRoutes?: ResolvedApiRoute[];
|
|
42
|
+
/**
|
|
43
|
+
* Dev-only phase-timing collector. When provided, the runtime records
|
|
44
|
+
* middleware/loader/render durations (ms) onto it so callers can emit a
|
|
45
|
+
* `Server-Timing` header. Leave unset in production — no timing work runs.
|
|
46
|
+
*/
|
|
47
|
+
timings?: PrachtPhaseTimings;
|
|
19
48
|
}
|
|
20
49
|
declare function handlePrachtRequest<TContext>(options: HandlePrachtRequestOptions<TContext>): Promise<Response>;
|
|
21
50
|
//#endregion
|
|
@@ -48,4 +77,4 @@ declare function prerenderApp(options: PrerenderAppOptions & {
|
|
|
48
77
|
withISGManifest: true;
|
|
49
78
|
}): Promise<PrerenderAppResult>;
|
|
50
79
|
//#endregion
|
|
51
|
-
export { prerenderApp as a, applyDefaultSecurityHeaders as c, PrerenderResult as i, PrerenderAppOptions as n, HandlePrachtRequestOptions as o, PrerenderAppResult as r, handlePrachtRequest as s, ISGManifestEntry as t };
|
|
80
|
+
export { prerenderApp as a, applyDefaultSecurityHeaders as c, PrerenderResult as i, PrachtPhaseTimings as l, PrerenderAppOptions as n, HandlePrachtRequestOptions as o, PrerenderAppResult as r, handlePrachtRequest as s, ISGManifestEntry as t, formatServerTimingHeader as u };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as matchApiRoute, c as resolveApp, n as buildPathFromSegments, o as matchAppRoute } from "./app-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { a as matchApiRoute, c as resolveApp, n as buildPathFromSegments, o as matchAppRoute } from "./app-CGRKjZ6Z.mjs";
|
|
2
|
+
import { S as withRouteResponseHeaders, _ as appendVaryHeader, b as applySecurityAndRouteHeaders, f as runMiddlewareChain, g as resolveRegistryModule, h as resolvePageJsUrls, l as mergeDocumentHeaders, m as resolvePageCssUrls, p as resolveDataFunctions, u as mergeHeadMetadata, v as applyDefaultSecurityHeaders, x as withDefaultSecurityHeaders, y as applyHeaders } from "./types-DEPXdbyp.mjs";
|
|
3
|
+
import { _ as ROUTE_STATE_REQUEST_HEADER, p as HYDRATION_STATE_ELEMENT_ID, s as buildRouteStateUrl, v as SAFE_METHODS } from "./prefetch-cache-BNLEp9H5.mjs";
|
|
4
|
+
import { d as buildRuntimeDiagnostics, f as createSerializedRouteError, h as shouldExposeServerErrors, m as normalizeRouteError, p as deserializeRouteError, s as PrachtRuntimeProvider } from "./navigation-state-BjwXTeVz.mjs";
|
|
5
5
|
import { h } from "preact";
|
|
6
6
|
//#region src/runtime-html.ts
|
|
7
7
|
function escapeHtml(str) {
|
|
@@ -260,6 +260,29 @@ function markdownResponse(source, initHeaders) {
|
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
|
+
//#region src/runtime-timing.ts
|
|
264
|
+
const PHASE_ORDER = [
|
|
265
|
+
"mw",
|
|
266
|
+
"loader",
|
|
267
|
+
"render"
|
|
268
|
+
];
|
|
269
|
+
/**
|
|
270
|
+
* Format collected phase timings as a standards-compliant `Server-Timing`
|
|
271
|
+
* header value, e.g. `mw;dur=1.2, loader;dur=14.8, render;dur=3.1`.
|
|
272
|
+
* Returns an empty string when nothing was recorded.
|
|
273
|
+
*/
|
|
274
|
+
function formatServerTimingHeader(timings) {
|
|
275
|
+
const entries = [];
|
|
276
|
+
for (const phase of PHASE_ORDER) {
|
|
277
|
+
const duration = timings[phase];
|
|
278
|
+
if (typeof duration === "number" && Number.isFinite(duration)) entries.push(`${phase};dur=${formatDuration(duration)}`);
|
|
279
|
+
}
|
|
280
|
+
return entries.join(", ");
|
|
281
|
+
}
|
|
282
|
+
function formatDuration(duration) {
|
|
283
|
+
return String(Math.max(0, Math.round(duration * 10) / 10));
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
263
286
|
//#region src/runtime.ts
|
|
264
287
|
const SAME_ORIGIN_FETCH_SITE = "same-origin";
|
|
265
288
|
/**
|
|
@@ -431,6 +454,7 @@ async function handlePrachtRequest(options) {
|
|
|
431
454
|
let shellModule;
|
|
432
455
|
let loaderFile;
|
|
433
456
|
let currentPhase = "middleware";
|
|
457
|
+
const timings = options.timings;
|
|
434
458
|
try {
|
|
435
459
|
const routeModulePromise = resolveRegistryModule(registry.routeModules, match.route.file);
|
|
436
460
|
const shellModulePromise = match.route.shellFile ? resolveRegistryModule(registry.shellModules, match.route.shellFile) : Promise.resolve(void 0);
|
|
@@ -445,7 +469,12 @@ async function handlePrachtRequest(options) {
|
|
|
445
469
|
currentPhase = "loader";
|
|
446
470
|
const { loader, loaderFile: resolvedLoaderFile } = await dataFunctionsPromise;
|
|
447
471
|
loaderFile = resolvedLoaderFile;
|
|
448
|
-
|
|
472
|
+
let loaderResult;
|
|
473
|
+
if (loader) {
|
|
474
|
+
const loaderStart = timings ? performance.now() : 0;
|
|
475
|
+
loaderResult = await loader(routeArgs);
|
|
476
|
+
if (timings) timings.loader = performance.now() - loaderStart;
|
|
477
|
+
}
|
|
449
478
|
if (loaderResult instanceof Response) return loaderResult;
|
|
450
479
|
const data = loaderResult;
|
|
451
480
|
if (isRouteStateRequest) return Response.json({ data });
|
|
@@ -517,7 +546,20 @@ async function handlePrachtRequest(options) {
|
|
|
517
546
|
modulePreloadUrls
|
|
518
547
|
}), 200, documentHeaders);
|
|
519
548
|
};
|
|
520
|
-
|
|
549
|
+
let terminal = pageTerminal;
|
|
550
|
+
let chainStart = 0;
|
|
551
|
+
if (timings) {
|
|
552
|
+
terminal = async () => {
|
|
553
|
+
const terminalStart = performance.now();
|
|
554
|
+
try {
|
|
555
|
+
return await pageTerminal();
|
|
556
|
+
} finally {
|
|
557
|
+
timings.render = performance.now() - terminalStart - (timings.loader ?? 0);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
chainStart = performance.now();
|
|
561
|
+
}
|
|
562
|
+
const response = await runMiddlewareChain({
|
|
521
563
|
context: pageContext,
|
|
522
564
|
middlewareFiles: match.route.middlewareFiles,
|
|
523
565
|
params: match.params,
|
|
@@ -526,8 +568,10 @@ async function handlePrachtRequest(options) {
|
|
|
526
568
|
route: match.route,
|
|
527
569
|
signal: requestSignal,
|
|
528
570
|
url,
|
|
529
|
-
terminal
|
|
530
|
-
})
|
|
571
|
+
terminal
|
|
572
|
+
});
|
|
573
|
+
if (timings) timings.mw = performance.now() - chainStart - (timings.render ?? 0) - (timings.loader ?? 0);
|
|
574
|
+
return normalizePageResponse(response, { isRouteStateRequest });
|
|
531
575
|
} catch (error) {
|
|
532
576
|
return renderRouteErrorResponse({
|
|
533
577
|
error,
|
|
@@ -643,4 +687,4 @@ async function collectSSGPaths(route, registry) {
|
|
|
643
687
|
return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
|
|
644
688
|
}
|
|
645
689
|
//#endregion
|
|
646
|
-
export { handlePrachtRequest as n, prerenderApp as t };
|
|
690
|
+
export { handlePrachtRequest as n, formatServerTimingHeader as r, prerenderApp as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as PrachtHydrationState } from "./runtime-context-
|
|
1
|
+
import { H as ResolvedPrachtApp, J as RouteId, P as NavigateOptions, at as RouteTarget } from "./types-B-sfVjaH.mjs";
|
|
2
|
+
import { t as PrachtHydrationState } from "./runtime-context-BYBwVA0M.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/router.d.ts
|
|
5
5
|
declare global {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { o as matchAppRoute, t as buildHref } from "./app-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { o as matchAppRoute, t as buildHref } from "./app-CGRKjZ6Z.mjs";
|
|
2
|
+
import { c as fetchPrachtRouteState, d as routeNeedsServerFetch, i as getCachedRouteState, u as parseSafeNavigationUrl } from "./prefetch-cache-BNLEp9H5.mjs";
|
|
3
|
+
import { a as settleNavigation, p as deserializeRouteError, r as createNavigationLocation, s as PrachtRuntimeProvider, t as beginLoadingNavigation } from "./navigation-state-BjwXTeVz.mjs";
|
|
4
|
+
import { r as registerPrefetchTarget } from "./prefetch-api-ChSCTbEB.mjs";
|
|
4
5
|
import { createContext, h, hydrate, options, render } from "preact";
|
|
5
|
-
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
|
6
|
+
import { useContext, useEffect, useLayoutEffect, useMemo, useState } from "preact/hooks";
|
|
6
7
|
import { Suspense } from "preact-suspense";
|
|
7
8
|
//#region src/hydration.ts
|
|
8
9
|
const MODE_HYDRATE$1 = 32;
|
|
@@ -200,6 +201,92 @@ function getVNodeName(vnode) {
|
|
|
200
201
|
return "Unknown";
|
|
201
202
|
}
|
|
202
203
|
//#endregion
|
|
204
|
+
//#region src/scroll-restoration.ts
|
|
205
|
+
const STORAGE_KEY = "pracht:scroll-positions";
|
|
206
|
+
const MAX_SCROLL_ENTRIES = 50;
|
|
207
|
+
const HISTORY_STATE_KEY = "__prachtScrollKey";
|
|
208
|
+
function readEntries(storage) {
|
|
209
|
+
if (!storage) return [];
|
|
210
|
+
let raw = null;
|
|
211
|
+
try {
|
|
212
|
+
raw = storage.getItem(STORAGE_KEY);
|
|
213
|
+
} catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
if (!raw) return [];
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(raw);
|
|
219
|
+
if (!Array.isArray(parsed)) return [];
|
|
220
|
+
return parsed.filter((entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && typeof entry[2] === "number");
|
|
221
|
+
} catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create a scroll position store backed by the given storage (normally
|
|
227
|
+
* `sessionStorage`). Storage failures (private mode, quota) degrade to an
|
|
228
|
+
* in-memory map for the current page lifetime.
|
|
229
|
+
*/
|
|
230
|
+
function createScrollPositionStore(storage, maxEntries = MAX_SCROLL_ENTRIES) {
|
|
231
|
+
const positions = /* @__PURE__ */ new Map();
|
|
232
|
+
for (const [key, x, y] of readEntries(storage)) positions.set(key, {
|
|
233
|
+
x,
|
|
234
|
+
y
|
|
235
|
+
});
|
|
236
|
+
function persist() {
|
|
237
|
+
if (!storage) return;
|
|
238
|
+
const entries = [];
|
|
239
|
+
for (const [key, position] of positions) entries.push([
|
|
240
|
+
key,
|
|
241
|
+
position.x,
|
|
242
|
+
position.y
|
|
243
|
+
]);
|
|
244
|
+
try {
|
|
245
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
get(key) {
|
|
250
|
+
return positions.get(key) ?? null;
|
|
251
|
+
},
|
|
252
|
+
set(key, position) {
|
|
253
|
+
positions.delete(key);
|
|
254
|
+
positions.set(key, position);
|
|
255
|
+
while (positions.size > maxEntries) {
|
|
256
|
+
const oldest = positions.keys().next();
|
|
257
|
+
if (oldest.done) break;
|
|
258
|
+
positions.delete(oldest.value);
|
|
259
|
+
}
|
|
260
|
+
persist();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function getSessionScrollStorage() {
|
|
265
|
+
if (typeof window === "undefined") return null;
|
|
266
|
+
try {
|
|
267
|
+
return window.sessionStorage ?? null;
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function generateScrollKey() {
|
|
273
|
+
return Math.random().toString(36).slice(2, 10);
|
|
274
|
+
}
|
|
275
|
+
/** Read the pracht scroll key from a `history.state` value, if present. */
|
|
276
|
+
function readScrollKeyFromHistoryState(state) {
|
|
277
|
+
if (!state || typeof state !== "object") return null;
|
|
278
|
+
const key = state[HISTORY_STATE_KEY];
|
|
279
|
+
return typeof key === "string" ? key : null;
|
|
280
|
+
}
|
|
281
|
+
/** Merge the pracht scroll key into an existing `history.state` value. */
|
|
282
|
+
function withScrollKeyInHistoryState(state, key) {
|
|
283
|
+
if (state && typeof state === "object" && !Array.isArray(state)) return {
|
|
284
|
+
...state,
|
|
285
|
+
[HISTORY_STATE_KEY]: key
|
|
286
|
+
};
|
|
287
|
+
return { [HISTORY_STATE_KEY]: key };
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
203
290
|
//#region src/router.ts
|
|
204
291
|
const NavigateContext = createContext(async () => {});
|
|
205
292
|
function useNavigate() {
|
|
@@ -232,11 +319,56 @@ async function initClientRouter(options) {
|
|
|
232
319
|
let routeStateVersion = 0;
|
|
233
320
|
let latestNavigationId = 0;
|
|
234
321
|
let activeNavigationAbort = null;
|
|
322
|
+
const scrollStore = createScrollPositionStore(getSessionScrollStorage());
|
|
323
|
+
if ("scrollRestoration" in history) history.scrollRestoration = "manual";
|
|
324
|
+
let currentScrollKey = readScrollKeyFromHistoryState(history.state) ?? "";
|
|
325
|
+
const hadExistingScrollKey = currentScrollKey !== "";
|
|
326
|
+
if (!hadExistingScrollKey) {
|
|
327
|
+
currentScrollKey = generateScrollKey();
|
|
328
|
+
try {
|
|
329
|
+
history.replaceState(withScrollKeyInHistoryState(history.state, currentScrollKey), "", window.location.href);
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
function saveScrollPosition() {
|
|
333
|
+
scrollStore.set(currentScrollKey, {
|
|
334
|
+
x: window.scrollX,
|
|
335
|
+
y: window.scrollY
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
window.addEventListener("pagehide", saveScrollPosition);
|
|
339
|
+
function restoreOrResetScroll(opts, browserUrl) {
|
|
340
|
+
if (opts?.preserveScroll) return;
|
|
341
|
+
if (opts?._popstate) {
|
|
342
|
+
const saved = scrollStore.get(currentScrollKey);
|
|
343
|
+
window.scrollTo(saved?.x ?? 0, saved?.y ?? 0);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const hashIndex = browserUrl.indexOf("#");
|
|
347
|
+
if (hashIndex !== -1) {
|
|
348
|
+
let id = browserUrl.slice(hashIndex + 1);
|
|
349
|
+
try {
|
|
350
|
+
id = decodeURIComponent(id);
|
|
351
|
+
} catch {}
|
|
352
|
+
const hashTarget = id ? document.getElementById(id) : null;
|
|
353
|
+
if (hashTarget && typeof hashTarget.scrollIntoView === "function") {
|
|
354
|
+
hashTarget.scrollIntoView();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
window.scrollTo(0, 0);
|
|
359
|
+
}
|
|
360
|
+
let afterCommitCallback = null;
|
|
235
361
|
function RouterRoot({ initialState }) {
|
|
236
362
|
const [routeState, setRouteState] = useState(initialState);
|
|
237
363
|
updateRouteState = setRouteState;
|
|
238
364
|
const navigateValue = useMemo(() => navigate, []);
|
|
239
365
|
const { Shell, Component, componentProps, data, params, routeId, url, version } = routeState;
|
|
366
|
+
useLayoutEffect(() => {
|
|
367
|
+
if (!afterCommitCallback) return;
|
|
368
|
+
const callback = afterCommitCallback;
|
|
369
|
+
afterCommitCallback = null;
|
|
370
|
+
callback();
|
|
371
|
+
}, [version]);
|
|
240
372
|
const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
|
|
241
373
|
return h(NavigateContext.Provider, { value: navigateValue }, h(PrachtRuntimeProvider, {
|
|
242
374
|
data,
|
|
@@ -337,69 +469,86 @@ async function initClientRouter(options) {
|
|
|
337
469
|
window.location.href = target.browserUrl;
|
|
338
470
|
return;
|
|
339
471
|
}
|
|
340
|
-
|
|
341
|
-
if (routeNeedsServerFetch(match.route)) statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl, { signal: abortController.signal });
|
|
342
|
-
else statePromise = Promise.resolve({
|
|
343
|
-
type: "data",
|
|
344
|
-
data: void 0
|
|
345
|
-
});
|
|
346
|
-
const routeModPromise = startRouteImport(match);
|
|
347
|
-
const shellModPromise = startShellImport(match);
|
|
348
|
-
let state = {
|
|
349
|
-
data: void 0,
|
|
350
|
-
error: null
|
|
351
|
-
};
|
|
472
|
+
const navigationToken = beginLoadingNavigation(createNavigationLocation(target.browserUrl));
|
|
352
473
|
try {
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
474
|
+
let statePromise;
|
|
475
|
+
if (routeNeedsServerFetch(match.route)) statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl, { signal: abortController.signal });
|
|
476
|
+
else statePromise = Promise.resolve({
|
|
477
|
+
type: "data",
|
|
478
|
+
data: void 0
|
|
479
|
+
});
|
|
480
|
+
const routeModPromise = startRouteImport(match);
|
|
481
|
+
const shellModPromise = startShellImport(match);
|
|
482
|
+
let state = {
|
|
483
|
+
data: void 0,
|
|
484
|
+
error: null
|
|
485
|
+
};
|
|
486
|
+
try {
|
|
487
|
+
const result = await statePromise;
|
|
488
|
+
if (navigationId !== latestNavigationId) return;
|
|
489
|
+
if (result.type === "redirect") {
|
|
490
|
+
if (result.location) {
|
|
491
|
+
const redirect = resolveRedirectTarget(result.location);
|
|
492
|
+
if (redirect.unsafe) {
|
|
493
|
+
console.error(`[pracht] refused to navigate to unsafe URL: ${result.location}`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (redirect.externalUrl) {
|
|
497
|
+
window.location.href = redirect.externalUrl;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (redirect.isCurrentLocation) return;
|
|
501
|
+
if (redirect.documentUrl) {
|
|
502
|
+
window.location.href = redirect.documentUrl;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (redirect.internalPath) {
|
|
506
|
+
await navigate(redirect.internalPath, opts);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
window.location.href = target.browserUrl;
|
|
373
510
|
return;
|
|
374
511
|
}
|
|
375
512
|
window.location.href = target.browserUrl;
|
|
376
513
|
return;
|
|
377
514
|
}
|
|
515
|
+
if (result.type === "error") state = {
|
|
516
|
+
data: void 0,
|
|
517
|
+
error: result.error
|
|
518
|
+
};
|
|
519
|
+
else state = {
|
|
520
|
+
data: result.data,
|
|
521
|
+
error: null
|
|
522
|
+
};
|
|
523
|
+
} catch {
|
|
524
|
+
if (abortController.signal.aborted || navigationId !== latestNavigationId) return;
|
|
378
525
|
window.location.href = target.browserUrl;
|
|
379
526
|
return;
|
|
380
527
|
}
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
528
|
+
if (navigationId !== latestNavigationId) return;
|
|
529
|
+
if (!opts?._popstate) {
|
|
530
|
+
saveScrollPosition();
|
|
531
|
+
if (opts?.replace) history.replaceState(withScrollKeyInHistoryState(history.state, currentScrollKey), "", target.browserUrl);
|
|
532
|
+
else {
|
|
533
|
+
const nextScrollKey = generateScrollKey();
|
|
534
|
+
history.pushState(withScrollKeyInHistoryState(null, nextScrollKey), "", target.browserUrl);
|
|
535
|
+
currentScrollKey = nextScrollKey;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const routeState = await resolveRouteState(match, state, target.requestUrl, routeModPromise, shellModPromise);
|
|
539
|
+
if (navigationId !== latestNavigationId) return;
|
|
540
|
+
if (!routeState) {
|
|
541
|
+
window.location.href = target.browserUrl;
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const commit = () => {
|
|
545
|
+
afterCommitCallback = () => restoreOrResetScroll(opts, target.browserUrl);
|
|
546
|
+
applyRouteState(routeState);
|
|
388
547
|
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return;
|
|
548
|
+
await commitWithOptionalViewTransition(commit, opts?.viewTransition ?? app.viewTransitions === true);
|
|
549
|
+
} finally {
|
|
550
|
+
settleNavigation(navigationToken);
|
|
393
551
|
}
|
|
394
|
-
if (navigationId !== latestNavigationId) return;
|
|
395
|
-
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", target.browserUrl);
|
|
396
|
-
else history.pushState(null, "", target.browserUrl);
|
|
397
|
-
const routeState = await resolveRouteState(match, state, target.requestUrl, routeModPromise, shellModPromise);
|
|
398
|
-
if (navigationId !== latestNavigationId) return;
|
|
399
|
-
if (routeState) {
|
|
400
|
-
applyRouteState(routeState);
|
|
401
|
-
window.scrollTo(0, 0);
|
|
402
|
-
} else window.location.href = target.browserUrl;
|
|
403
552
|
}
|
|
404
553
|
const initialTarget = resolveBrowserRouteTarget(options.initialState.url);
|
|
405
554
|
const initialRequestUrl = initialTarget?.requestUrl ?? options.initialState.url;
|
|
@@ -468,21 +617,65 @@ async function initClientRouter(options) {
|
|
|
468
617
|
}
|
|
469
618
|
if (url.origin !== window.location.origin) return;
|
|
470
619
|
e.preventDefault();
|
|
471
|
-
|
|
620
|
+
const navOptions = {};
|
|
621
|
+
if (anchor.hasAttribute("data-pracht-preserve-scroll")) navOptions.preserveScroll = true;
|
|
622
|
+
if (anchor.hasAttribute("data-pracht-view-transition")) navOptions.viewTransition = true;
|
|
623
|
+
navigate(url.pathname + url.search + url.hash, navOptions);
|
|
472
624
|
});
|
|
473
625
|
window.addEventListener("popstate", () => {
|
|
626
|
+
saveScrollPosition();
|
|
627
|
+
let nextScrollKey = readScrollKeyFromHistoryState(history.state);
|
|
628
|
+
if (!nextScrollKey) {
|
|
629
|
+
nextScrollKey = generateScrollKey();
|
|
630
|
+
try {
|
|
631
|
+
history.replaceState(withScrollKeyInHistoryState(history.state, nextScrollKey), "", window.location.href);
|
|
632
|
+
} catch {}
|
|
633
|
+
}
|
|
634
|
+
currentScrollKey = nextScrollKey;
|
|
474
635
|
navigate(window.location.pathname + window.location.search + window.location.hash, { _popstate: true });
|
|
475
636
|
});
|
|
476
637
|
window.__PRACHT_NAVIGATE__ = navigate;
|
|
477
638
|
window.__PRACHT_ROUTER_READY__ = true;
|
|
639
|
+
document.documentElement.setAttribute("data-pracht-hydrated", "true");
|
|
640
|
+
if (hadExistingScrollKey) {
|
|
641
|
+
const savedPosition = scrollStore.get(currentScrollKey);
|
|
642
|
+
if (savedPosition) window.scrollTo(savedPosition.x, savedPosition.y);
|
|
643
|
+
}
|
|
478
644
|
const warmModules = (match) => {
|
|
479
645
|
startRouteImport(match);
|
|
480
646
|
startShellImport(match);
|
|
481
647
|
};
|
|
482
|
-
|
|
648
|
+
registerPrefetchTarget(app, warmModules);
|
|
649
|
+
import("./prefetch-COYeuvQK.mjs").then(({ setupPrefetching }) => {
|
|
483
650
|
setupPrefetching(app, warmModules);
|
|
484
651
|
});
|
|
485
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* Commit a navigation's DOM update, optionally wrapped in
|
|
655
|
+
* `document.startViewTransition()`. Falls back to a plain commit when view
|
|
656
|
+
* transitions are disabled or unsupported. Resolves once the DOM update has
|
|
657
|
+
* been applied (not when the transition animation finishes).
|
|
658
|
+
*/
|
|
659
|
+
async function commitWithOptionalViewTransition(commit, useViewTransition) {
|
|
660
|
+
const doc = document;
|
|
661
|
+
if (!useViewTransition || typeof doc.startViewTransition !== "function") {
|
|
662
|
+
commit();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
let committed = false;
|
|
666
|
+
let transition;
|
|
667
|
+
try {
|
|
668
|
+
transition = doc.startViewTransition(async () => {
|
|
669
|
+
committed = true;
|
|
670
|
+
commit();
|
|
671
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
672
|
+
});
|
|
673
|
+
} catch {}
|
|
674
|
+
try {
|
|
675
|
+
await transition?.updateCallbackDone;
|
|
676
|
+
} catch {}
|
|
677
|
+
if (!committed) commit();
|
|
678
|
+
}
|
|
486
679
|
function resolveBrowserRouteTarget(to) {
|
|
487
680
|
if (typeof window === "undefined") return null;
|
|
488
681
|
try {
|