@pracht/core 0.8.0 → 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.
Files changed (38) hide show
  1. package/README.md +5 -1
  2. package/dist/app-BN3pjxtw.d.mts +24 -0
  3. package/dist/{app-BvC1uQG5.mjs → app-CGRKjZ6Z.mjs} +97 -8
  4. package/dist/app-graph-BlaCcGUZ.mjs +52 -0
  5. package/dist/app-graph-DBoDsHJ5.d.mts +38 -0
  6. package/dist/browser.d.mts +7 -6
  7. package/dist/browser.mjs +8 -7
  8. package/dist/client.d.mts +3 -3
  9. package/dist/client.mjs +3 -3
  10. package/dist/dev-404.d.mts +26 -0
  11. package/dist/dev-404.mjs +166 -0
  12. package/dist/devtools.d.mts +8 -0
  13. package/dist/devtools.mjs +156 -0
  14. package/dist/error-overlay.d.mts +38 -1
  15. package/dist/error-overlay.mjs +134 -5
  16. package/dist/index.d.mts +9 -7
  17. package/dist/index.mjs +9 -7
  18. package/dist/manifest.d.mts +2 -1
  19. package/dist/manifest.mjs +1 -1
  20. package/dist/{runtime-context-B5pREhcM.mjs → navigation-state-BjwXTeVz.mjs} +74 -2
  21. package/dist/{prefetch-B50kX9RL.mjs → prefetch-COYeuvQK.mjs} +44 -27
  22. package/dist/prefetch-api-Bf-P7XFo.d.mts +39 -0
  23. package/dist/prefetch-api-ChSCTbEB.mjs +69 -0
  24. package/dist/{prefetch-cache-DzP2Bj9H.mjs → prefetch-cache-BNLEp9H5.mjs} +12 -1
  25. package/dist/{prerender-B8_CqYwd.d.mts → prerender-BV42B9jK.d.mts} +31 -2
  26. package/dist/{prerender-D3E2H96p.mjs → prerender-C8BpANz3.mjs} +57 -15
  27. package/dist/{router-BcUmg1kB.d.mts → router-B_Aj5TtY.d.mts} +2 -2
  28. package/dist/router-E5Wh-dOR.mjs +694 -0
  29. package/dist/{runtime-context-ytVd7GmZ.d.mts → runtime-context-BYBwVA0M.d.mts} +1 -1
  30. package/dist/{runtime-middleware-aeBdgjYr.d.mts → runtime-middleware-pYubngnL.d.mts} +57 -4
  31. package/dist/server.d.mts +7 -5
  32. package/dist/server.mjs +6 -5
  33. package/dist/{app-BWriseLj.d.mts → types-B-sfVjaH.d.mts} +42 -22
  34. package/dist/{types-idmK5omD.mjs → types-DEPXdbyp.mjs} +42 -21
  35. package/package.json +9 -1
  36. package/dist/hydration-M1QzbQ1O.d.mts +0 -23
  37. package/dist/router-B7J4YYwg.mjs +0 -418
  38. /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 { trimMapToSize as a, navigateToClientLocation as c, EMPTY_ROUTE_PARAMS as d, HYDRATION_STATE_ELEMENT_ID as f, SAFE_METHODS as h, getCachedRouteState as i, parseSafeNavigationUrl as l, ROUTE_STATE_REQUEST_HEADER as m, cacheRouteState as n, buildRouteStateUrl as o, ROUTE_STATE_CACHE_CONTROL as p, clearPrefetchCache as r, fetchPrachtRouteState as s, EMPTY_ROUTE_STATE_PROMISE as t, routeNeedsServerFetch as u };
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 { H as ModuleRegistry, W as PrachtApp, X as ResolvedApiRoute, lt as RouteRevalidate } from "./app-BWriseLj.mjs";
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-BvC1uQG5.mjs";
2
- import { _ as applyDefaultSecurityHeaders, b as withDefaultSecurityHeaders, c as mergeDocumentHeaders, d as runMiddlewareChain, f as resolveDataFunctions, g as appendVaryHeader, h as resolveRegistryModule, l as mergeHeadMetadata, m as resolvePageJsUrls, p as resolvePageCssUrls, v as applyHeaders, x as withRouteResponseHeaders, y as applySecurityAndRouteHeaders } from "./types-idmK5omD.mjs";
3
- import { f as HYDRATION_STATE_ELEMENT_ID, h as SAFE_METHODS, m as ROUTE_STATE_REQUEST_HEADER, o as buildRouteStateUrl } from "./prefetch-cache-DzP2Bj9H.mjs";
4
- import { a as buildRuntimeDiagnostics, c as normalizeRouteError, l as shouldExposeServerErrors, o as createSerializedRouteError, s as deserializeRouteError, t as PrachtRuntimeProvider } from "./runtime-context-B5pREhcM.mjs";
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) {
@@ -249,11 +249,9 @@ function prefersMarkdown(accept) {
249
249
  if (!html) return true;
250
250
  return md.quality >= html.quality;
251
251
  }
252
- function markdownResponse(source) {
253
- const headers = new Headers({
254
- "content-type": "text/markdown; charset=utf-8",
255
- "cache-control": "public, max-age=0, must-revalidate"
256
- });
252
+ function markdownResponse(source, initHeaders) {
253
+ const headers = new Headers(initHeaders);
254
+ headers.set("content-type", "text/markdown; charset=utf-8");
257
255
  appendVaryHeader(headers, "Accept");
258
256
  applyDefaultSecurityHeaders(headers);
259
257
  return new Response(source, {
@@ -262,6 +260,29 @@ function markdownResponse(source) {
262
260
  });
263
261
  }
264
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
265
286
  //#region src/runtime.ts
266
287
  const SAME_ORIGIN_FETCH_SITE = "same-origin";
267
288
  /**
@@ -433,6 +454,7 @@ async function handlePrachtRequest(options) {
433
454
  let shellModule;
434
455
  let loaderFile;
435
456
  let currentPhase = "middleware";
457
+ const timings = options.timings;
436
458
  try {
437
459
  const routeModulePromise = resolveRegistryModule(registry.routeModules, match.route.file);
438
460
  const shellModulePromise = match.route.shellFile ? resolveRegistryModule(registry.shellModules, match.route.shellFile) : Promise.resolve(void 0);
@@ -444,17 +466,22 @@ async function handlePrachtRequest(options) {
444
466
  currentPhase = "render";
445
467
  routeModule = await routeModulePromise;
446
468
  if (!routeModule) throw new Error("Route module not found");
447
- if (!isRouteStateRequest && typeof routeModule.markdown === "string" && prefersMarkdown(options.request.headers.get("accept"))) return markdownResponse(routeModule.markdown);
448
469
  currentPhase = "loader";
449
470
  const { loader, loaderFile: resolvedLoaderFile } = await dataFunctionsPromise;
450
471
  loaderFile = resolvedLoaderFile;
451
- const loaderResult = loader ? await loader(routeArgs) : void 0;
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
+ }
452
478
  if (loaderResult instanceof Response) return loaderResult;
453
479
  const data = loaderResult;
454
480
  if (isRouteStateRequest) return Response.json({ data });
455
481
  currentPhase = "render";
456
482
  shellModule = await shellModulePromise;
457
483
  const [head, documentHeaders] = await Promise.all([mergeHeadMetadata(shellModule, routeModule, routeArgs, data), mergeDocumentHeaders(shellModule, routeModule, routeArgs, data)]);
484
+ if (!isRouteStateRequest && typeof routeModule.markdown === "string" && prefersMarkdown(options.request.headers.get("accept"))) return markdownResponse(routeModule.markdown, documentHeaders);
458
485
  const cssUrls = resolvePageCssUrls(options.cssManifest, match.route.shellFile, match.route.file);
459
486
  const modulePreloadUrls = resolvePageJsUrls(options.jsManifest, match.route.shellFile, match.route.file);
460
487
  if (match.route.render === "spa") {
@@ -519,7 +546,20 @@ async function handlePrachtRequest(options) {
519
546
  modulePreloadUrls
520
547
  }), 200, documentHeaders);
521
548
  };
522
- return normalizePageResponse(await runMiddlewareChain({
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({
523
563
  context: pageContext,
524
564
  middlewareFiles: match.route.middlewareFiles,
525
565
  params: match.params,
@@ -528,8 +568,10 @@ async function handlePrachtRequest(options) {
528
568
  route: match.route,
529
569
  signal: requestSignal,
530
570
  url,
531
- terminal: pageTerminal
532
- }), { isRouteStateRequest });
571
+ terminal
572
+ });
573
+ if (timings) timings.mw = performance.now() - chainStart - (timings.render ?? 0) - (timings.loader ?? 0);
574
+ return normalizePageResponse(response, { isRouteStateRequest });
533
575
  } catch (error) {
534
576
  return renderRouteErrorResponse({
535
577
  error,
@@ -645,4 +687,4 @@ async function collectSSGPaths(route, registry) {
645
687
  return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
646
688
  }
647
689
  //#endregion
648
- export { handlePrachtRequest as n, prerenderApp as t };
690
+ export { handlePrachtRequest as n, formatServerTimingHeader as r, prerenderApp as t };
@@ -1,5 +1,5 @@
1
- import { U as NavigateOptions, Z as ResolvedPrachtApp, dt as RouteTarget, nt as RouteId } from "./app-BWriseLj.mjs";
2
- import { t as PrachtHydrationState } from "./runtime-context-ytVd7GmZ.mjs";
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 {