@kasperrt/smolter 0.4.1 → 0.4.3

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 (86) hide show
  1. package/.generated/client/deployment.ts +40 -25
  2. package/.generated/core/environment.ts +1 -0
  3. package/.generated/core/navigation.ts +1 -0
  4. package/.generated/core/resource-types.ts +4 -0
  5. package/.generated/core/resources.ts +2 -0
  6. package/.generated/core/responses.ts +12 -0
  7. package/.generated/core/utils.ts +5 -2
  8. package/.generated/files/index.ts +5 -3
  9. package/.generated/preact/await-fallback.tsx +33 -0
  10. package/.generated/preact/await.tsx +23 -5
  11. package/.generated/preact/client.tsx +15 -4
  12. package/.generated/preact/deferred-hydration.ts +14 -1
  13. package/.generated/preact/document.tsx +10 -0
  14. package/.generated/preact/index.ts +6 -2
  15. package/.generated/preact/lazy-route.ts +7 -0
  16. package/.generated/preact/navigation.tsx +6 -0
  17. package/.generated/preact/root-context.ts +4 -0
  18. package/.generated/preact/route-hooks.tsx +12 -2
  19. package/.generated/preact/route-types.ts +14 -0
  20. package/.generated/preact/router.tsx +3 -0
  21. package/.generated/react/data.ts +5 -1
  22. package/.generated/react/index.ts +11 -1
  23. package/.generated/react/outlet.ts +48 -0
  24. package/.generated/react/resources/components.tsx +12 -0
  25. package/.generated/server/application.tsx +13 -0
  26. package/.generated/server/assets.ts +7 -0
  27. package/.generated/server/handler.ts +3 -0
  28. package/.generated/server/http.ts +2 -0
  29. package/.generated/server/node.ts +5 -2
  30. package/.generated/server/preact.ts +5 -2
  31. package/.generated/vite/application/lazy-files.ts +10 -17
  32. package/.generated/vite/index.ts +14 -0
  33. package/CHANGELOG.md +18 -0
  34. package/README.md +38 -18
  35. package/dist/client/deployment.js +30 -22
  36. package/dist/core/environment.d.ts +1 -0
  37. package/dist/core/navigation.d.ts +1 -0
  38. package/dist/core/resource-types.d.ts +4 -0
  39. package/dist/core/resources.d.ts +2 -0
  40. package/dist/core/resources.js +2 -0
  41. package/dist/core/responses.d.ts +12 -0
  42. package/dist/core/responses.js +5 -0
  43. package/dist/core/utils.d.ts +5 -2
  44. package/dist/core/utils.js +1 -2
  45. package/dist/files/index.js +4 -3
  46. package/dist/preact/await-fallback.d.ts +4 -0
  47. package/dist/preact/await-fallback.js +29 -0
  48. package/dist/preact/await.d.ts +5 -1
  49. package/dist/preact/await.js +22 -3
  50. package/dist/preact/client.d.ts +9 -1
  51. package/dist/preact/client.js +8 -3
  52. package/dist/preact/deferred-hydration.d.ts +1 -1
  53. package/dist/preact/deferred-hydration.js +7 -1
  54. package/dist/preact/document.d.ts +10 -0
  55. package/dist/preact/document.js +10 -1
  56. package/dist/preact/index.d.ts +6 -2
  57. package/dist/preact/lazy-route.d.ts +2 -0
  58. package/dist/preact/lazy-route.js +6 -0
  59. package/dist/preact/navigation.d.ts +6 -0
  60. package/dist/preact/navigation.js +2 -0
  61. package/dist/preact/root-context.d.ts +1 -0
  62. package/dist/preact/route-hooks.d.ts +10 -0
  63. package/dist/preact/route-hooks.js +13 -3
  64. package/dist/preact/route-types.d.ts +14 -0
  65. package/dist/preact/router.d.ts +3 -0
  66. package/dist/preact/router.js +3 -1
  67. package/dist/react/data.d.ts +5 -1
  68. package/dist/react/index.d.ts +6 -1
  69. package/dist/react/index.js +2 -1
  70. package/dist/react/outlet.d.ts +16 -0
  71. package/dist/react/outlet.js +26 -0
  72. package/dist/react/resources/components.d.ts +12 -0
  73. package/dist/react/resources/components.js +11 -0
  74. package/dist/server/application.d.ts +3 -0
  75. package/dist/server/application.js +10 -0
  76. package/dist/server/assets.d.ts +2 -0
  77. package/dist/server/assets.js +6 -0
  78. package/dist/server/handler.d.ts +3 -0
  79. package/dist/server/http.d.ts +2 -0
  80. package/dist/server/http.js +2 -0
  81. package/dist/server/node.d.ts +5 -1
  82. package/dist/server/preact.d.ts +5 -1
  83. package/dist/vite/application/lazy-files.js +5 -11
  84. package/dist/vite/index.js +6 -0
  85. package/jsr.json +1 -1
  86. package/package.json +6 -5
@@ -1,6 +1,14 @@
1
1
  import type { NavigationRouter, Router } from "../core/index.ts";
2
2
  import { deploymentCheck } from "./deployment-check.ts";
3
3
 
4
+ interface WatcherState {
5
+ sequence: number;
6
+ mutations: number;
7
+ cleanup?: () => void;
8
+ }
9
+
10
+ const watchers = new WeakMap<NavigationRouter, WatcherState>();
11
+
4
12
  /** Build identity and the fresh, non-cacheable manifest URL emitted by deploymentPlugin. */
5
13
  export interface DeploymentOptions {
6
14
  version: string;
@@ -18,47 +26,50 @@ export function watchDeployment(
18
26
  ): () => void {
19
27
  if (!options || typeof window === "undefined") return () => {};
20
28
 
29
+ const state = watchers.get(router) ?? { sequence: 0, mutations: 0 };
30
+ state.cleanup?.();
31
+ watchers.set(router, state);
21
32
  const check = deploymentCheck(options);
22
33
  const { changed, reload } = check;
23
34
  const read = router.getSnapshot;
24
35
  const navigate = router.navigate;
25
36
  const dispose = router.dispose;
26
- let sequence = 0;
27
37
  let stopped = false;
28
38
  let replay = false;
29
- let mutations = 0;
30
39
  const dataRouter = router as NavigationRouter &
31
- Partial<Pick<Router, "createFetcher">>;
40
+ Partial<Pick<Router, "createFetcher" | "submit">>;
32
41
  const createFetcher = dataRouter.createFetcher;
42
+ const submit = dataRouter.submit;
43
+ const trackSubmission =
44
+ (submit: Router["submit"]): Router["submit"] =>
45
+ (...args) => {
46
+ state.sequence++;
47
+ state.mutations++;
48
+
49
+ return submit(...args).finally(() => {
50
+ state.mutations--;
51
+ });
52
+ };
53
+ const guardedSubmit = submit && trackSubmission(submit);
33
54
  const guardedFetcher =
34
55
  createFetcher &&
35
56
  (() => {
36
57
  const fetcher = createFetcher();
37
- const submit = fetcher.submit;
38
-
39
- fetcher.submit = (...args) => {
40
- sequence++;
41
- mutations++;
42
-
43
- return submit(...args).finally(() => {
44
- mutations--;
45
- });
46
- };
58
+ fetcher.submit = trackSubmission(fetcher.submit);
47
59
 
48
60
  return fetcher;
49
61
  });
50
62
 
63
+ if (guardedSubmit) dataRouter.submit = guardedSubmit;
51
64
  if (guardedFetcher) dataRouter.createFetcher = guardedFetcher;
52
- const valid = (current: number) => current === sequence && !stopped;
53
- const canReload = (current: number) => valid(current) && mutations === 0;
54
- const unsubscribe = router.subscribe(() => {
55
- if (read().state === "submitting") sequence++;
56
- });
65
+ const valid = (current: number) => current === state.sequence && !stopped;
66
+ const canReload = (current: number) =>
67
+ valid(current) && state.mutations === 0;
57
68
 
58
69
  const guarded: NavigationRouter["navigate"] = async (href, operation) => {
59
70
  const target = new URL(href, router.getSnapshot().url);
60
71
  const currentURL = new URL(router.getSnapshot().url);
61
- const current = ++sequence;
72
+ const current = ++state.sequence;
62
73
 
63
74
  if (
64
75
  target.origin === location.origin &&
@@ -70,7 +81,7 @@ export function watchDeployment(
70
81
  return;
71
82
  }
72
83
 
73
- if (current === sequence && !stopped) await navigate(href, operation);
84
+ if (current === state.sequence && !stopped) await navigate(href, operation);
74
85
  };
75
86
 
76
87
  const pop = (event: PopStateEvent) => {
@@ -78,7 +89,7 @@ export function watchDeployment(
78
89
 
79
90
  const target = new URL(location.href);
80
91
  const previous = new URL(read().url);
81
- const current = ++sequence;
92
+ const current = ++state.sequence;
82
93
 
83
94
  if (
84
95
  target.pathname === previous.pathname &&
@@ -105,7 +116,7 @@ export function watchDeployment(
105
116
  // consume the recovery marker before a click records its destination.
106
117
  if (read().state !== "loading") return;
107
118
 
108
- const current = sequence;
119
+ const current = state.sequence;
109
120
  const target = new URL(read().url);
110
121
  void changed().then((updated) => {
111
122
  if (updated)
@@ -120,14 +131,17 @@ export function watchDeployment(
120
131
  };
121
132
 
122
133
  function cleanup() {
134
+ if (stopped) return;
135
+ if (state.cleanup === cleanup) state.cleanup = undefined;
123
136
  stopped = true;
124
- sequence++;
137
+ state.sequence++;
125
138
  check.dispose();
126
- unsubscribe();
127
139
  window.removeEventListener("popstate", pop, true);
128
140
  window.removeEventListener("vite:preloadError", preloadError);
129
141
 
130
- if (dataRouter.createFetcher === guardedFetcher)
142
+ if (guardedSubmit && dataRouter.submit === guardedSubmit)
143
+ dataRouter.submit = submit;
144
+ if (guardedFetcher && dataRouter.createFetcher === guardedFetcher)
131
145
  dataRouter.createFetcher = createFetcher;
132
146
  if (router.navigate === guarded) router.navigate = navigate;
133
147
  if (router.dispose === guardedDispose) router.dispose = dispose;
@@ -138,6 +152,7 @@ export function watchDeployment(
138
152
  dispose();
139
153
  }
140
154
 
155
+ state.cleanup = cleanup;
141
156
  router.navigate = guarded;
142
157
  router.dispose = guardedDispose;
143
158
  window.addEventListener("popstate", pop, true);
@@ -1,3 +1,4 @@
1
+ /** Application base URLs, navigation handoff and document-transition settings shared by browser integrations. */
1
2
  export interface ApplicationEnvironment {
2
3
  resolveURL?: (url: URL) => URL;
3
4
  isDocumentHandoff?: (from: URL, to: URL) => boolean;
@@ -7,6 +7,7 @@ import { createStore } from "./store.ts";
7
7
  import type { NavigateOptions, Router } from "./types.ts";
8
8
  import { navigationClick } from "./url.ts";
9
9
 
10
+ /** Browser history operation: POP, PUSH or REPLACE. */
10
11
  export type NavigationType = "POP" | "PUSH" | "REPLACE";
11
12
 
12
13
  export interface NavigationSnapshot {
@@ -1,16 +1,19 @@
1
1
  export type Params = Readonly<Record<string, string>>;
2
2
 
3
+ /** Resource parameters and cancellation signal supplied to a loader. */
3
4
  export interface LoaderFunctionArgs {
4
5
  params: Params;
5
6
  signal: AbortSignal;
6
7
  }
7
8
 
9
+ /** Submitted input, resource parameters and cancellation signal supplied to an action. */
8
10
  export interface ActionFunctionArgs<Input = unknown> {
9
11
  input: Input;
10
12
  params: Params;
11
13
  signal: AbortSignal;
12
14
  }
13
15
 
16
+ /** Successful resource data or a normalized resource failure. */
14
17
  export interface DataResult<Data> {
15
18
  data: Data | null;
16
19
  error: string;
@@ -23,6 +26,7 @@ export interface ResourceModule {
23
26
  shouldRevalidate?: (args: ShouldRevalidateFunctionArgs) => boolean;
24
27
  }
25
28
 
29
+ /** Navigation or mutation context used to decide whether loader data should refresh. */
26
30
  export interface ShouldRevalidateFunctionArgs {
27
31
  actionResult: DataResult<unknown>;
28
32
  defaultShouldRevalidate: boolean;
@@ -18,6 +18,7 @@ const IDLE_ROUTE_STATE: RouteState = {
18
18
  unexpectedError: false,
19
19
  };
20
20
 
21
+ /** Manage keyed resource reads, submissions, subscriptions and cancellation independently of navigation. */
21
22
  export class ResourceRouter {
22
23
  readonly #cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
23
24
  readonly #controllers = new Map<string, AbortController>();
@@ -427,6 +428,7 @@ export class ResourceRouter {
427
428
  }
428
429
  }
429
430
 
431
+ /** Create an independent resource-data router from registered modules; this does not manage URL history. */
430
432
  export function createResourceRouter(
431
433
  routes: ReadonlyMap<string, ResourceModule>,
432
434
  ): ResourceRouter {
@@ -1,19 +1,28 @@
1
+ /**
2
+ * HTTP response helpers and loader result normalization.
3
+ *
4
+ * @module
5
+ */
6
+ /** Normalized route failure with an HTTP status, status text and optional response data. */
1
7
  export interface RouteError extends Error {
2
8
  status: number;
3
9
  statusText: string;
4
10
  data?: unknown;
5
11
  }
6
12
 
13
+ /** Loader value paired with HTTP response metadata. */
7
14
  export interface DataResponse<T> {
8
15
  value: T;
9
16
  init: ResponseInit;
10
17
  __routeData: true;
11
18
  }
12
19
 
20
+ /** Wrap loader data with HTTP status and headers without creating a Response body. */
13
21
  export function data<T>(value: T, init: ResponseInit = {}): DataResponse<T> {
14
22
  return { value, init, __routeData: true };
15
23
  }
16
24
 
25
+ /** Create an empty redirect Response with a Location header; defaults to HTTP 302. */
17
26
  export function redirect(
18
27
  location: string,
19
28
  init: number | ResponseInit = 302,
@@ -29,10 +38,12 @@ export function redirect(
29
38
  });
30
39
  }
31
40
 
41
+ /** Identify normalized Error objects carrying an HTTP status. */
32
42
  export function isRouteErrorResponse(value: unknown): value is RouteError {
33
43
  return value instanceof Error && "status" in value;
34
44
  }
35
45
 
46
+ /** Normalize transport responses and wrapped data, optionally resolving nested deferred values. */
36
47
  export async function normalizeResult(
37
48
  value: unknown,
38
49
  resolveDeferred = true,
@@ -68,6 +79,7 @@ export async function normalizeResult(
68
79
  }
69
80
 
70
81
  // Smolter transfers JSON snapshots. Resolve deferred fields before serialization.
82
+ /** Recursively resolve promises in arrays and plain objects before JSON serialization. */
71
83
  export async function resolveData(value: unknown): Promise<unknown> {
72
84
  const resolved = await value;
73
85
 
@@ -1,5 +1,8 @@
1
- /** Framework-independent, browser-safe utilities. @module */
2
- export * from "./errors.ts";
1
+ /**
2
+ * Routing patterns, query strings and serialized data utilities.
3
+ *
4
+ * @module
5
+ */
3
6
  export * from "./match.ts";
4
7
  export * from "./serialize.ts";
5
8
  export * from "./url.ts";
@@ -47,10 +47,12 @@ export async function flatRoutes(
47
47
  name = name.replace(flatFile, "");
48
48
  }
49
49
 
50
- if (/[()[\]:*?]/.test(name))
50
+ const escaped = name.replaceAll("[.]", "\0");
51
+
52
+ if (/[()[\]:*?]/.test(escaped))
51
53
  return new Error(`Unsupported flat route name: ${name}`);
52
54
 
53
- const segments = name.split(".");
55
+ const segments = escaped.split(".");
54
56
  const parts: string[] = [];
55
57
 
56
58
  for (const [index, segment] of segments.entries()) {
@@ -64,7 +66,7 @@ export async function flatRoutes(
64
66
 
65
67
  if (segment.startsWith("_")) continue;
66
68
 
67
- const part = segment.replace(/_$/, "");
69
+ const part = segment.replace(/_$/, "").replaceAll("\0", ".");
68
70
  parts.push(part === "$" ? "*" : part.replace(/^\$/, ":"));
69
71
  }
70
72
 
@@ -0,0 +1,33 @@
1
+ /** @jsxImportSource preact */
2
+ import { useEffect, useState } from "preact/hooks";
3
+
4
+ import type { AwaitProps } from "./await.tsx";
5
+
6
+ type Result<T> = { source: Promise<T> } & ({ value: T } | { error: unknown });
7
+
8
+ export function AwaitFallback<T>({
9
+ resolve,
10
+ fallback,
11
+ errorElement,
12
+ children,
13
+ }: AwaitProps<T> & { resolve: Promise<T> }) {
14
+ const [result, setResult] = useState<Result<T>>();
15
+ useEffect(() => {
16
+ let current = true;
17
+ void resolve.then(
18
+ (value) => {
19
+ if (current) setResult({ source: resolve, value });
20
+ },
21
+ (error: unknown) => {
22
+ if (current) setResult({ source: resolve, error });
23
+ },
24
+ );
25
+ return () => {
26
+ current = false;
27
+ };
28
+ }, [resolve]);
29
+ if (!result || result.source !== resolve) return fallback;
30
+ if ("value" in result) return children(result.value as Awaited<T>);
31
+ if (errorElement !== undefined) return errorElement;
32
+ throw result.error;
33
+ }
@@ -1,10 +1,15 @@
1
+ /** @jsxImportSource preact */
1
2
  import type { ComponentChildren } from "preact";
2
- import { use } from "preact/compat";
3
+ import { use, useContext } from "preact/compat";
4
+ import { AwaitFallback } from "./await-fallback.tsx";
5
+ import { Application } from "./root-context.ts";
3
6
 
4
7
  export interface AwaitProps<T> {
5
8
  resolve: T | Promise<T>;
6
9
  children: (value: Awaited<T>) => ComponentChildren;
7
10
  errorElement?: ComponentChildren;
11
+ /** Component-owned pending content, safe during early hydration. Pass null to render nothing. */
12
+ fallback?: ComponentChildren;
8
13
  }
9
14
 
10
15
  type Resolution = { value: unknown } | { error: unknown };
@@ -12,13 +17,26 @@ type Resolution = { value: unknown } | { error: unknown };
12
17
  // Cache only the error-as-data adapter. Preact owns pending and resolved state.
13
18
  const resolutions = new WeakMap<Promise<unknown>, Promise<Resolution>>();
14
19
 
15
- export function Await<T>({
20
+ /** Render a value or promise. An explicit fallback owns pending content during early hydration; errorElement handles rejection. */
21
+ export function Await<T>(props: AwaitProps<T>): ComponentChildren {
22
+ const app = useContext(Application);
23
+ if (!(props.resolve instanceof Promise))
24
+ return props.children(props.resolve as Awaited<T>);
25
+ if (!("fallback" in props))
26
+ return <SuspendedAwait {...props} resolve={props.resolve} />;
27
+ if (app?.deferredBoundary)
28
+ return app.deferredBoundary(
29
+ <SuspendedAwait {...props} resolve={props.resolve} />,
30
+ props.fallback,
31
+ );
32
+ return <AwaitFallback {...props} resolve={props.resolve} />;
33
+ }
34
+
35
+ function SuspendedAwait<T>({
16
36
  resolve,
17
37
  children,
18
38
  errorElement,
19
- }: AwaitProps<T>): ComponentChildren {
20
- if (!(resolve instanceof Promise)) return children(resolve as Awaited<T>);
21
-
39
+ }: AwaitProps<T> & { resolve: Promise<T> }): ComponentChildren {
22
40
  let pending = resolutions.get(resolve);
23
41
 
24
42
  if (!pending) {
@@ -1,4 +1,8 @@
1
- /** Browser mounting and hydration for Preact applications. @module */
1
+ /**
2
+ * Preact document hydration, browser history and deployment recovery.
3
+ *
4
+ * @module
5
+ */
2
6
  /** @jsxImportSource preact */
3
7
 
4
8
  import {
@@ -19,18 +23,25 @@ import { prepareRoutes, RouterApplication } from "./router.tsx";
19
23
  export * from "../client/index.ts";
20
24
  export { configureApplication } from "../core/environment.ts";
21
25
 
26
+ /** Browser hydration settings for hash history, deployment recovery and deferred-data timeout. */
22
27
  export interface MountApplicationOptions {
23
28
  hash?: boolean;
24
29
  deployment?: DeploymentOptions | null;
30
+ /** Total initial deferred-data budget in integer milliseconds; not an inactivity timeout. Defaults to 30,000. */
31
+ deferredTimeout?: number;
25
32
  }
26
33
 
34
+ /** Create the browser application router and hydrate the document, installing recovery before initial lazy loading. */
27
35
  export async function mountApplication(
28
36
  routes: AppRoute[],
29
37
  rootModule?: RootModule,
30
38
  options: boolean | MountApplicationOptions = false,
31
39
  ): Promise<Router> {
32
- const { hash = false, deployment = null } =
33
- typeof options === "boolean" ? { hash: options } : options;
40
+ const {
41
+ hash = false,
42
+ deployment = null,
43
+ deferredTimeout,
44
+ } = typeof options === "boolean" ? { hash: options } : options;
34
45
  const initialURL = new URL(
35
46
  hash ? location.hash.slice(1) || "/" : location.href,
36
47
  location.origin,
@@ -48,7 +59,7 @@ export async function mountApplication(
48
59
  routes = prepareRoutes(routes);
49
60
  const element = document.getElementById("smolter-data");
50
61
  const initial = element?.textContent
51
- ? readHydration(element.textContent)
62
+ ? readHydration(element.textContent, deferredTimeout)
52
63
  : undefined;
53
64
  const hydration: Hydration | undefined = initial?.hydration;
54
65
 
@@ -11,7 +11,12 @@ export interface DeferredWindow extends Window {
11
11
  }
12
12
 
13
13
  /** Restore promise identity before hydrating Suspense boundaries. */
14
- export function readHydration(json: string) {
14
+ export function readHydration(json: string, timeout = 30_000) {
15
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 2_147_483_647)
16
+ throw new RangeError(
17
+ "deferredTimeout must be an integer between 1 and 2147483647 ms",
18
+ );
19
+
15
20
  const pending = new Map<number, PendingValue>();
16
21
  const queue = ((window as DeferredWindow).__smolterDeferred ??= []);
17
22
 
@@ -54,12 +59,20 @@ export function readHydration(json: string) {
54
59
 
55
60
  const hydration = JSON.parse(json, revive);
56
61
  const dispose = () => {
62
+ clearTimeout(deadline);
57
63
  window.removeEventListener("DOMContentLoaded", dispose);
58
64
  for (const target of pending.values())
59
65
  target.reject(new Error("Hydration stream closed"));
60
66
  pending.clear();
61
67
  queue.push = () => 0;
62
68
  };
69
+ const deadline = pending.size
70
+ ? setTimeout(() => {
71
+ for (const target of pending.values())
72
+ target.reject(new Error("Hydration stream timed out"));
73
+ dispose();
74
+ }, timeout)
75
+ : undefined;
63
76
  const receive = (packet: Settlement) => {
64
77
  const [id, value, error] = packet;
65
78
  if (id === -1) return dispose();
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Preact document metadata, resource links and hydration scripts.
3
+ *
4
+ * @module
5
+ */
1
6
  /** @jsxImportSource preact */
2
7
 
3
8
  import { applicationEnvironment } from "../core/environment.ts";
@@ -34,6 +39,7 @@ function renderMeta(item: MetaDescriptor, index: number) {
34
39
  });
35
40
  }
36
41
 
42
+ /** Render root or active-route metadata in the document head. */
37
43
  export function Meta(): import("preact").JSX.Element {
38
44
  const { routes, rootModule } = useApplication();
39
45
  const snapshot = useSnapshot();
@@ -55,6 +61,7 @@ export function Meta(): import("preact").JSX.Element {
55
61
  return <Fragment>{values.map(renderMeta)}</Fragment>;
56
62
  }
57
63
 
64
+ /** Render stylesheet and other link descriptors from the application root. */
58
65
  export function Links(): import("preact").JSX.Element {
59
66
  const { rootModule } = useApplication();
60
67
 
@@ -67,10 +74,12 @@ export function Links(): import("preact").JSX.Element {
67
74
  );
68
75
  }
69
76
 
77
+ /** Nonce and browser entry options for the document hydration scripts. */
70
78
  export interface ScriptProps {
71
79
  nonce?: string;
72
80
  }
73
81
 
82
+ /** Render the hydration snapshot and browser entry script with an optional CSP nonce. */
74
83
  export function Scripts({ nonce }: ScriptProps): import("preact").JSX.Element {
75
84
  const { root, rootError, router, entry, streaming, initialData } =
76
85
  useApplication();
@@ -119,6 +128,7 @@ export function Scripts({ nonce }: ScriptProps): import("preact").JSX.Element {
119
128
  );
120
129
  }
121
130
 
131
+ /** Reset scroll after navigation unless the navigation requests preservation. */
122
132
  export function ScrollRestoration(_props: ScriptProps): null {
123
133
  const { router } = useApplication();
124
134
  const { url, state } = useSnapshot();
@@ -1,4 +1,8 @@
1
- /** Framework components and typed data hooks. @module */
1
+ /**
2
+ * Preact routing components, typed data hooks and application integration.
3
+ *
4
+ * @module
5
+ */
2
6
  export type * from "../core/index.ts";
3
7
  export {
4
8
  createDataRouter,
@@ -30,5 +34,5 @@ export {
30
34
  useSubmit,
31
35
  } from "./route-hooks.tsx";
32
36
  export * from "./route-types.ts";
33
- export { Outlet, RouterApplication } from "./router.tsx";
37
+ export { Outlet, type OutletProps, RouterApplication } from "./router.tsx";
34
38
  export * from "./routes.ts";
@@ -1,3 +1,8 @@
1
+ /**
2
+ * On-demand Preact route modules with shared intent prefetching.
3
+ *
4
+ * @module
5
+ */
1
6
  import { serverRoute } from "../client/index.ts";
2
7
  import type { DataArgs, RouteModule, Router } from "../core/index.ts";
3
8
  import { type ComponentType, createElement } from "preact";
@@ -6,6 +11,7 @@ import type { AppRoute } from "./route-types.ts";
6
11
  import type { ErrorProps } from "./routes.ts";
7
12
  import { useSyncExternalStore } from "./runtime.ts";
8
13
 
14
+ /** Lazy route path, server transport endpoint and available handler capabilities. */
9
15
  export interface LazyRouteOptions {
10
16
  path: string;
11
17
  dataEndpoint?: string;
@@ -36,6 +42,7 @@ function LazyView() {
36
42
  return Component && createElement(Component, {});
37
43
  }
38
44
 
45
+ /** Create a route that imports its module on demand and shares the import with intent prefetching. */
39
46
  export function lazyRoute(
40
47
  options: LazyRouteOptions,
41
48
  importer: () => Promise<LoadedRoute>,
@@ -15,11 +15,13 @@ export type { NavigateOptions } from "../core/index.ts";
15
15
 
16
16
  export type { NavigationType } from "../core/navigation.ts";
17
17
 
18
+ /** Navigate to a URL or move through browser history using a numeric offset. */
18
19
  export type NavigateFunction = (
19
20
  to: string | number | Partial<Pick<URL, "pathname" | "search" | "hash">>,
20
21
  options?: NavigateOptions,
21
22
  ) => void;
22
23
 
24
+ /** Anchor properties and router navigation options, including document reload and prefetch controls. */
23
25
  export interface LinkProps extends Omit<ComponentProps<"a">, "href" | "role"> {
24
26
  to?: string;
25
27
  href?: string;
@@ -47,11 +49,13 @@ export interface LinkProps extends Omit<ComponentProps<"a">, "href" | "role"> {
47
49
  reloadDocument?: boolean;
48
50
  }
49
51
 
52
+ /** Active and pending flags supplied to NavLink render callbacks. */
50
53
  export interface NavLinkRenderProps {
51
54
  isActive: boolean;
52
55
  isPending: boolean;
53
56
  }
54
57
 
58
+ /** Link options with matching rules and state-aware class or children callbacks. */
55
59
  export interface NavLinkProps
56
60
  extends Omit<LinkProps, "className" | "children"> {
57
61
  end?: boolean;
@@ -59,6 +63,7 @@ export interface NavLinkProps
59
63
  children?: ReactNode | ((state: NavLinkRenderProps) => ReactNode);
60
64
  }
61
65
 
66
+ /** Render an anchor with same-origin router navigation and optional intent prefetching. */
62
67
  export function Link(options: LinkProps): ReactNode {
63
68
  const {
64
69
  href: hrefProp,
@@ -115,6 +120,7 @@ export function Link(options: LinkProps): ReactNode {
115
120
  });
116
121
  }
117
122
 
123
+ /** Render a Link with active-route state and aria-current for the matching destination. */
118
124
  export function NavLink({
119
125
  href,
120
126
  to = href ?? "/",
@@ -8,6 +8,10 @@ export interface ApplicationContext {
8
8
  root: unknown;
9
9
  rootError?: boolean;
10
10
  streaming?: boolean;
11
+ deferredBoundary?: (
12
+ children: import("preact").ComponentChildren,
13
+ fallback: import("preact").ComponentChildren,
14
+ ) => import("preact").ComponentChildren;
11
15
  initialData?: string;
12
16
  rootModule?: RootModule;
13
17
  hydration?: Hydration;
@@ -15,6 +15,7 @@ import { OutletContext, useApplication } from "./root-context.ts";
15
15
  import type { RouteData } from "./route-types.ts";
16
16
  import { useContext, useMemo } from "./runtime.ts";
17
17
 
18
+ /** Read root loader data from the application context. */
18
19
  export function useRootData<T = unknown>(): RouteData<T> {
19
20
  return useApplication().root as RouteData<T>;
20
21
  }
@@ -37,10 +38,12 @@ export function useRouteLoaderData<T = unknown>(
37
38
 
38
39
  export { useSnapshot };
39
40
 
41
+ /** Read the current route error for an error boundary. */
40
42
  export function useRouteError(): Error | undefined {
41
43
  return useSnapshot("error");
42
44
  }
43
45
 
46
+ /** Read the current page navigation or submission state. */
44
47
  export function useNavigation(): {
45
48
  state: "idle" | "loading" | "submitting";
46
49
  location: URL | undefined;
@@ -58,24 +61,27 @@ export function useNavigation(): {
58
61
  );
59
62
  }
60
63
 
64
+ /** Read the active flat route match and loader data; this is not a nested layout match chain. */
61
65
  export function useMatches(): {
62
66
  id: string;
63
67
  data: unknown;
64
68
  params: import("../core/index.ts").Params;
65
69
  }[] {
66
- const { routes } = useApplication();
70
+ const router = useRouter();
67
71
  const snapshot = useSnapshot();
68
- const route = routes.find((item) => item.path === snapshot.path);
72
+ const route = router.routes.find((item) => item.path === snapshot.path);
69
73
 
70
74
  return [
71
75
  { id: route?.id ?? "", data: snapshot.data, params: snapshot.params },
72
76
  ];
73
77
  }
74
78
 
79
+ /** Read context supplied by the nearest Outlet. */
75
80
  export function useOutletContext<T>(): T {
76
81
  return useContext(OutletContext) as T;
77
82
  }
78
83
 
84
+ /** Return a function that submits form data through the current router. */
79
85
  export function useSubmit(): (
80
86
  value: Submission,
81
87
  options?: SubmissionOptions,
@@ -103,6 +109,7 @@ export function useSubmit(): (
103
109
  );
104
110
  }
105
111
 
112
+ /** Read the current router location as a URL. */
106
113
  export function useURLLocation(): URL & { state: unknown; key: string } {
107
114
  const href = useSnapshot("url");
108
115
  const snapshot = useNavigationSnapshot(useRouter<Router>());
@@ -117,6 +124,7 @@ export function useURLLocation(): URL & { state: unknown; key: string } {
117
124
  );
118
125
  }
119
126
 
127
+ /** Return the application navigation function, including history offsets and relative destinations. */
120
128
  export function useNavigate(): NavigateFunction {
121
129
  const router = useRouter<Router>();
122
130
 
@@ -139,6 +147,7 @@ export function useNavigate(): NavigateFunction {
139
147
  );
140
148
  }
141
149
 
150
+ /** Read URL search parameters and update them through router navigation. */
142
151
  export function useSearchParams(): [
143
152
  URLSearchParams,
144
153
  (
@@ -165,6 +174,7 @@ export function useSearchParams(): [
165
174
  return [params, set];
166
175
  }
167
176
 
177
+ /** Read the last browser history operation. */
168
178
  export function useNavigationType(): NavigationType {
169
179
  return useNavigationSnapshot(useRouter<Router>()).type;
170
180
  }