@kasperrt/smolter 0.4.2 → 0.4.4

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 (151) hide show
  1. package/.generated/client/hash-history.ts +27 -0
  2. package/.generated/client/hydration.ts +98 -0
  3. package/.generated/client/root-data.ts +52 -0
  4. package/.generated/core/environment.ts +1 -0
  5. package/.generated/core/navigation.ts +1 -0
  6. package/.generated/core/prepare-routes.ts +50 -0
  7. package/.generated/core/presentation.ts +1 -1
  8. package/.generated/core/responses.ts +12 -0
  9. package/.generated/core/utils.ts +5 -2
  10. package/.generated/preact/await.tsx +1 -0
  11. package/.generated/preact/client.tsx +8 -27
  12. package/.generated/preact/deferred-hydration.ts +1 -98
  13. package/.generated/preact/document.tsx +10 -0
  14. package/.generated/preact/index.ts +11 -2
  15. package/.generated/preact/lazy-route.ts +7 -0
  16. package/.generated/preact/navigation.tsx +19 -5
  17. package/.generated/preact/root-data.ts +1 -45
  18. package/.generated/preact/route-hooks.tsx +12 -2
  19. package/.generated/preact/route-types.ts +14 -0
  20. package/.generated/preact/router.tsx +5 -48
  21. package/.generated/react/client.tsx +130 -0
  22. package/.generated/react/document.tsx +235 -0
  23. package/.generated/react/fetcher.ts +29 -7
  24. package/.generated/react/hooks.ts +20 -18
  25. package/.generated/react/index.ts +28 -2
  26. package/.generated/react/lazy-route.ts +116 -0
  27. package/.generated/react/link.ts +1 -38
  28. package/.generated/react/navigation-state.ts +13 -0
  29. package/.generated/react/navigation.ts +145 -0
  30. package/.generated/react/outlet.ts +48 -0
  31. package/.generated/react/root-context.ts +22 -0
  32. package/.generated/react/route-hooks.ts +159 -0
  33. package/.generated/react/route-types.ts +91 -0
  34. package/.generated/react/router.ts +50 -0
  35. package/.generated/react/runtime.ts +1 -0
  36. package/.generated/server/application.tsx +18 -250
  37. package/.generated/server/assets.ts +7 -0
  38. package/.generated/server/dispatch.ts +87 -0
  39. package/.generated/server/handler.ts +3 -0
  40. package/.generated/server/http.ts +2 -0
  41. package/.generated/server/index.ts +2 -0
  42. package/.generated/server/node.ts +5 -2
  43. package/.generated/server/preact.ts +5 -2
  44. package/.generated/server/react.ts +34 -0
  45. package/.generated/server/render-application.ts +203 -0
  46. package/.generated/vite/application/lazy-files.ts +2 -1
  47. package/.generated/vite/application/vite.ts +12 -4
  48. package/.generated/vite/react.ts +11 -0
  49. package/CHANGELOG.md +20 -0
  50. package/README.md +48 -48
  51. package/dist/client/hash-history.d.ts +2 -0
  52. package/dist/client/hash-history.js +26 -0
  53. package/dist/client/hydration.d.ts +10 -0
  54. package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
  55. package/dist/client/root-data.d.ts +14 -0
  56. package/dist/{preact → client}/root-data.js +1 -1
  57. package/dist/core/environment.d.ts +1 -0
  58. package/dist/core/navigation.d.ts +1 -0
  59. package/dist/core/prepare-routes.d.ts +7 -0
  60. package/dist/core/prepare-routes.js +28 -0
  61. package/dist/core/presentation.js +1 -1
  62. package/dist/core/responses.d.ts +12 -0
  63. package/dist/core/responses.js +5 -0
  64. package/dist/core/utils.d.ts +5 -2
  65. package/dist/core/utils.js +1 -2
  66. package/dist/preact/await.d.ts +1 -0
  67. package/dist/preact/await.js +1 -0
  68. package/dist/preact/client.d.ts +7 -1
  69. package/dist/preact/client.js +11 -27
  70. package/dist/preact/deferred-hydration.d.ts +1 -10
  71. package/dist/preact/document.d.ts +10 -0
  72. package/dist/preact/document.js +11 -2
  73. package/dist/preact/index.d.ts +6 -2
  74. package/dist/preact/index.js +1 -1
  75. package/dist/preact/lazy-route.d.ts +2 -0
  76. package/dist/preact/lazy-route.js +6 -0
  77. package/dist/preact/navigation-state.js +1 -1
  78. package/dist/preact/navigation.d.ts +6 -0
  79. package/dist/preact/navigation.js +9 -5
  80. package/dist/preact/root-data.d.ts +1 -8
  81. package/dist/preact/route-hooks.d.ts +10 -0
  82. package/dist/preact/route-hooks.js +13 -3
  83. package/dist/preact/route-types.d.ts +14 -0
  84. package/dist/preact/router.d.ts +5 -2
  85. package/dist/preact/router.js +4 -26
  86. package/dist/react/client.d.ts +20 -0
  87. package/dist/react/client.js +86 -0
  88. package/dist/react/document.d.ts +25 -0
  89. package/dist/react/document.js +169 -0
  90. package/dist/react/fetcher.d.ts +8 -4
  91. package/dist/react/fetcher.js +12 -1
  92. package/dist/react/form.js +1 -1
  93. package/dist/react/hooks.d.ts +7 -6
  94. package/dist/react/index.d.ts +13 -2
  95. package/dist/react/index.js +7 -2
  96. package/dist/react/lazy-route.d.ts +22 -0
  97. package/dist/react/lazy-route.js +57 -0
  98. package/dist/react/link.d.ts +1 -12
  99. package/dist/react/navigation-state.d.ts +2 -0
  100. package/dist/react/navigation-state.js +9 -0
  101. package/dist/react/navigation.d.ts +33 -0
  102. package/dist/react/navigation.js +64 -0
  103. package/dist/react/outlet.d.ts +16 -0
  104. package/dist/react/outlet.js +26 -0
  105. package/dist/react/root-context.d.ts +14 -0
  106. package/dist/react/root-context.js +10 -0
  107. package/dist/react/route-hooks.d.ts +34 -0
  108. package/dist/react/route-hooks.js +89 -0
  109. package/dist/react/route-types.d.ts +69 -0
  110. package/dist/react/router.d.ts +10 -0
  111. package/dist/react/router.js +22 -0
  112. package/dist/react/runtime.d.ts +1 -1
  113. package/dist/react/runtime.js +1 -1
  114. package/dist/server/application.d.ts +4 -5
  115. package/dist/server/application.js +9 -154
  116. package/dist/server/assets.d.ts +2 -0
  117. package/dist/server/assets.js +6 -0
  118. package/dist/server/dispatch.d.ts +5 -0
  119. package/dist/server/dispatch.js +50 -0
  120. package/dist/server/handler.d.ts +3 -0
  121. package/dist/server/http.d.ts +2 -0
  122. package/dist/server/http.js +2 -0
  123. package/dist/server/index.d.ts +1 -0
  124. package/dist/server/index.js +2 -1
  125. package/dist/server/node.d.ts +5 -1
  126. package/dist/server/preact.d.ts +5 -1
  127. package/dist/server/preact.js +2 -1
  128. package/dist/server/react.d.ts +10 -0
  129. package/dist/server/react.js +22 -0
  130. package/dist/server/render-application.d.ts +31 -0
  131. package/dist/server/render-application.js +118 -0
  132. package/dist/vite/application/lazy-files.d.ts +1 -1
  133. package/dist/vite/application/lazy-files.js +2 -2
  134. package/dist/vite/application/vite.d.ts +2 -1
  135. package/dist/vite/application/vite.js +12 -5
  136. package/dist/vite/react.d.ts +4 -0
  137. package/dist/vite/react.js +9 -1
  138. package/jsr.json +10 -6
  139. package/package.json +21 -12
  140. package/.generated/core/resource-types.ts +0 -44
  141. package/.generated/core/resources.ts +0 -444
  142. package/.generated/react/data.ts +0 -25
  143. package/.generated/react/resources/components.tsx +0 -404
  144. package/dist/core/resource-types.d.ts +0 -37
  145. package/dist/core/resources.d.ts +0 -19
  146. package/dist/core/resources.js +0 -345
  147. package/dist/react/data.d.ts +0 -5
  148. package/dist/react/data.js +0 -3
  149. package/dist/react/link.js +0 -27
  150. package/dist/react/resources/components.d.ts +0 -64
  151. package/dist/react/resources/components.js +0 -239
@@ -14,8 +14,15 @@ import {
14
14
  type Router,
15
15
  type SubmitOptions,
16
16
  } from "../core/index.ts";
17
+ import {
18
+ type Submission,
19
+ type SubmissionOptions,
20
+ submissionBody,
21
+ submissionSearch,
22
+ } from "../core/submission.ts";
17
23
  import { useRouter } from "./context.ts";
18
24
  import { Form, type FormProps } from "./form.ts";
25
+ import type { RouteData } from "./route-types.ts";
19
26
 
20
27
  import {
21
28
  type ComponentType,
@@ -26,7 +33,7 @@ import {
26
33
  } from "./runtime.ts";
27
34
 
28
35
  /** Action method and optional explicit route URL for a fetcher submission. */
29
- export interface FetcherSubmitOptions extends SubmitOptions {
36
+ export interface FetcherSubmitOptions extends SubmissionOptions {
30
37
  action?: string;
31
38
  }
32
39
 
@@ -35,7 +42,7 @@ export interface TypedFetcher<T> extends Omit<FetcherSnapshot, "data"> {
35
42
  data?: T;
36
43
  Form: ComponentType<FormProps>;
37
44
  load: (url?: string) => Promise<Result>;
38
- submit: (body: FormData, options?: FetcherSubmitOptions) => Promise<Result>;
45
+ submit: (body: Submission, options?: FetcherSubmitOptions) => Promise<Result>;
39
46
  }
40
47
 
41
48
  /** Create a fetcher for independent route reads and mutations; its data stays local. */
@@ -47,9 +54,7 @@ export function useFetcher<T extends RouteModule>(
47
54
  >;
48
55
 
49
56
  /** Create a fetcher for independent route reads and mutations; its data stays local. */
50
- export function useFetcher<
51
- T extends AnyDataFunction = DataFunction,
52
- >(): TypedFetcher<DataOf<T>>;
57
+ export function useFetcher<T = DataFunction>(): TypedFetcher<RouteData<T>>;
53
58
 
54
59
  /** Create a fetcher for independent route reads and mutations; its data stays local. */
55
60
  export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
@@ -68,8 +73,22 @@ export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
68
73
  Form: (props: FormProps) =>
69
74
  createElement(Form, { action: target(), ...props, fetcher: store }),
70
75
  load: (url?: string) => store.load(url ?? target()),
71
- submit: (body: FormData, options?: FetcherSubmitOptions) =>
72
- store.submit(options?.action ?? target(), body, options),
76
+ submit: (body: Submission, options: FetcherSubmitOptions = {}) => {
77
+ const action = options.action ?? target();
78
+ const data = submissionBody(body, options.encType);
79
+ const method = options.method?.toUpperCase();
80
+
81
+ if (method === "GET") {
82
+ const url = new URL(action, router.getSnapshot().url);
83
+ url.search = submissionSearch(data);
84
+
85
+ return store.load(url.href);
86
+ }
87
+
88
+ return store.submit(action, data, {
89
+ method: method as SubmitOptions["method"],
90
+ });
91
+ },
73
92
  };
74
93
  }, [router, path]);
75
94
 
@@ -83,3 +102,6 @@ export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
83
102
 
84
103
  return { ...fetcher, ...state };
85
104
  }
105
+
106
+ /** A fetcher with a bound form and inferred result data. */
107
+ export type FetcherWithComponents<T> = TypedFetcher<RouteData<T>>;
@@ -5,43 +5,43 @@
5
5
 
6
6
  import type {
7
7
  AnyDataFunction,
8
- DataOf,
9
8
  Route,
10
9
  RouteModule,
11
10
  Router,
12
11
  Snapshot,
13
12
  } from "../core/index.ts";
14
-
15
13
  import { useRouter, useSnapshot } from "./context.ts";
14
+ import type { RouteData } from "./route-types.ts";
16
15
 
17
16
  /** A route module or loader function used for data inference. */
18
17
  export type LoaderSource = RouteModule | AnyDataFunction;
19
18
 
20
19
  /** Infer server and client loader data from a route module or function. */
21
20
  export type LoaderData<T> = T extends AnyDataFunction
22
- ? DataOf<T>
21
+ ? RouteData<T>
23
22
  : T extends RouteModule
24
- ? DataOf<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>
25
- : unknown;
23
+ ? RouteData<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>
24
+ : RouteData<T>;
26
25
 
27
26
  /** Infer server and client action data from a route module or function. */
28
27
  export type ActionData<T> = T extends AnyDataFunction
29
- ? DataOf<T>
28
+ ? RouteData<T>
30
29
  : T extends RouteModule
31
- ? DataOf<Extract<T["action"] | T["clientAction"], AnyDataFunction>>
32
- : unknown;
30
+ ? RouteData<Extract<T["action"] | T["clientAction"], AnyDataFunction>>
31
+ : RouteData<T>;
33
32
  /** Pass a route for inference, or use useLoaderData<typeof loader>() in its module. */
34
- export function useLoaderData<T extends LoaderSource = RouteModule>(
35
- route?: T,
36
- ): LoaderData<T> | undefined {
37
- return useSnapshot("data", (route as Route | undefined)?.path) as
38
- | LoaderData<T>
39
- | undefined;
33
+ export function useLoaderData<T = RouteModule>(
34
+ route?: T & LoaderSource,
35
+ ): LoaderData<T> {
36
+ return useSnapshot(
37
+ "data",
38
+ (route as Route | undefined)?.path,
39
+ ) as LoaderData<T>;
40
40
  }
41
41
 
42
42
  /** Read the last page action result, optionally inferring its type from a route. */
43
- export function useActionData<T extends LoaderSource = RouteModule>(
44
- route?: T,
43
+ export function useActionData<T = RouteModule>(
44
+ route?: T & LoaderSource,
45
45
  ): ActionData<T> | undefined {
46
46
  return useSnapshot("actionData", (route as Route | undefined)?.path) as
47
47
  | ActionData<T>
@@ -49,8 +49,10 @@ export function useActionData<T extends LoaderSource = RouteModule>(
49
49
  }
50
50
 
51
51
  /** Read path parameters for the active route. */
52
- export function useParams(): Snapshot["params"] {
53
- return useSnapshot("params");
52
+ export function useParams<
53
+ T extends Snapshot["params"] = Snapshot["params"],
54
+ >(): T {
55
+ return useSnapshot("params") as T;
54
56
  }
55
57
 
56
58
  /** Read the current path, search, and hash together with the navigate function. */
@@ -1,10 +1,21 @@
1
- /** Framework components and typed data hooks. @module */
1
+ /**
2
+ * React routing components, typed data hooks and forms.
3
+ *
4
+ * @module
5
+ */
2
6
  export type * from "../core/index.ts";
3
7
  export {
4
8
  createDataRouter,
5
9
  createRouter,
6
10
  defineRoute,
7
11
  } from "../core/index.ts";
12
+ export type { ApplicationEnvironment } from "../core/environment.ts";
13
+ export * from "../core/responses.ts";
14
+ export {
15
+ readSubmission,
16
+ type Submission,
17
+ type SubmissionOptions,
18
+ } from "../core/submission.ts";
8
19
  export {
9
20
  RouterProvider,
10
21
  type RouterProviderProps,
@@ -13,5 +24,20 @@ export {
13
24
  export * from "./fetcher.ts";
14
25
  export * from "./form.ts";
15
26
  export * from "./hooks.ts";
16
- export * from "./link.ts";
27
+ export * from "./navigation.ts";
28
+ export {
29
+ Outlet,
30
+ type OutletProps,
31
+ useMatches,
32
+ useOutletContext,
33
+ } from "./outlet.ts";
34
+ export {
35
+ useNavigation,
36
+ useRootData,
37
+ useRouteError,
38
+ useRouteLoaderData,
39
+ useSubmit,
40
+ } from "./route-hooks.ts";
41
+ export * from "./route-types.ts";
42
+ export { type ApplicationProps, RouterApplication } from "./router.ts";
17
43
  export * from "./routes.ts";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * On-demand React route modules with shared intent prefetching.
3
+ *
4
+ * @module
5
+ */
6
+ import { serverRoute } from "../client/index.ts";
7
+ import type { DataArgs, RouteModule, Router } from "../core/index.ts";
8
+ import { useRouter } from "./context.ts";
9
+ import type { AppRoute } from "./route-types.ts";
10
+ import type { ErrorProps } from "./routes.ts";
11
+ import {
12
+ type ComponentType,
13
+ createElement,
14
+ useSyncExternalStore,
15
+ } from "./runtime.ts";
16
+
17
+ /** Lazy route path, server transport endpoint and available handler capabilities. */
18
+ export interface LazyRouteOptions {
19
+ path: string;
20
+ dataEndpoint?: string;
21
+ id?: string;
22
+ loader?: boolean;
23
+ action?: boolean;
24
+ clientAction?: boolean;
25
+ meta?: boolean;
26
+ shouldRevalidate?: boolean;
27
+ ErrorBoundary?: boolean;
28
+ }
29
+
30
+ type LoadedRoute = RouteModule & Pick<AppRoute, "meta">;
31
+
32
+ interface LazyAppRoute extends AppRoute {
33
+ resolveComponent: () => ComponentType | undefined;
34
+ }
35
+
36
+ // A shared wrapper preserves component identity across routes using the same view.
37
+ function LazyView() {
38
+ const router = useRouter<Router>();
39
+ const { path } = useSyncExternalStore(
40
+ router.subscribe,
41
+ router.getSnapshot,
42
+ router.getSnapshot,
43
+ );
44
+ const route = router.routes.find((item) => item.path === path) as
45
+ | LazyAppRoute
46
+ | undefined;
47
+ const Component = route?.resolveComponent();
48
+
49
+ return Component && createElement(Component, {});
50
+ }
51
+
52
+ /** Create a route that imports its module on demand and shares the import with intent prefetching. */
53
+ export function lazyRoute(
54
+ options: LazyRouteOptions,
55
+ importer: () => Promise<LoadedRoute>,
56
+ ): LazyAppRoute {
57
+ let module: LoadedRoute | undefined;
58
+ let pending: Promise<void> | undefined;
59
+
60
+ function preload() {
61
+ pending ??= importer()
62
+ .then((loaded) => {
63
+ module = serverRoute(options.path, loaded, {
64
+ loader: options.loader,
65
+ action: options.action,
66
+ dataEndpoint: options.dataEndpoint,
67
+ });
68
+ })
69
+ .catch((error: unknown) => {
70
+ pending = undefined;
71
+ throw error;
72
+ });
73
+
74
+ return pending;
75
+ }
76
+
77
+ async function run(args: DataArgs, action = false) {
78
+ await preload();
79
+
80
+ if (args.signal.aborted) return new Error("Navigation cancelled");
81
+
82
+ const handler = action
83
+ ? (module!.clientAction ?? module!.action)
84
+ : (module!.clientLoader ?? module!.loader);
85
+
86
+ // serverRoute supplies serverLoader/serverAction to client handlers.
87
+ return (handler as ((args: DataArgs) => unknown) | undefined)?.(args);
88
+ }
89
+
90
+ function ErrorBoundary(props: ErrorProps) {
91
+ const Component = module?.ErrorBoundary as
92
+ | ComponentType<ErrorProps>
93
+ | undefined;
94
+
95
+ if (!Component) return props.error.message;
96
+
97
+ return createElement(Component, props);
98
+ }
99
+
100
+ return {
101
+ path: options.path,
102
+ id: options.id,
103
+ preload,
104
+ default: LazyView,
105
+ resolveComponent: () => module?.default as ComponentType | undefined,
106
+ clientLoader: (args) => run(args),
107
+ ...((options.action || options.clientAction) && {
108
+ clientAction: (args: DataArgs) => run(args, true),
109
+ }),
110
+ ...(options.meta && { meta: (args) => module?.meta?.(args) ?? [] }),
111
+ ...(options.shouldRevalidate && {
112
+ shouldRevalidate: (args) => module?.shouldRevalidate?.(args) ?? true,
113
+ }),
114
+ ...(options.ErrorBoundary && { ErrorBoundary }),
115
+ };
116
+ }
@@ -1,38 +1 @@
1
- /**
2
- * React components and typed data hooks. Import from @kasperrt/smolter/react.
3
- * @module
4
- */
5
-
6
- import { internalURL, navigationClick } from "../core/url.ts";
7
- import { useRouter } from "./context.ts";
8
-
9
- import {
10
- type AnchorHTMLAttributes,
11
- createElement,
12
- type MouseEvent,
13
- type ReactNode,
14
- } from "./runtime.ts";
15
-
16
- /** Anchor attributes with internal navigation and optional history replacement. */
17
- export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
18
- href: string;
19
- replace?: boolean;
20
- }
21
-
22
- /** Render an anchor that handles internal navigation and preserves native modified clicks. */
23
- export function Link(options: LinkProps): ReactNode {
24
- const { href, replace, onClick, ...props } = options;
25
- const router = useRouter();
26
- const click = (event: MouseEvent<HTMLAnchorElement>) => {
27
- onClick?.(event);
28
-
29
- if (!navigationClick(event, event.currentTarget)) return;
30
-
31
- if (!internalURL(router.getSnapshot().url, href)) return;
32
-
33
- event.preventDefault();
34
- void router.navigate(href, options);
35
- };
36
-
37
- return createElement("a", { ...props, href, onClick: click });
38
- }
1
+ export { Link, type LinkProps } from "./navigation.ts";
@@ -0,0 +1,13 @@
1
+ import type { Router } from "../core/index.ts";
2
+ import { navigationStore } from "../core/navigation.ts";
3
+ import { useSyncExternalStore } from "./runtime.ts";
4
+
5
+ export function useNavigationSnapshot(router: Router) {
6
+ const store = navigationStore(router);
7
+
8
+ return useSyncExternalStore(
9
+ store.subscribe,
10
+ store.getSnapshot,
11
+ store.getSnapshot,
12
+ );
13
+ }
@@ -0,0 +1,145 @@
1
+ import { matchRoutes, type NavigateOptions } from "../core/index.ts";
2
+ import {
3
+ handoffDocument,
4
+ resolveApplicationURL,
5
+ } from "../core/environment.ts";
6
+ import { navigationClick } from "../core/url.ts";
7
+ import { useRouter, useSnapshot } from "./context.ts";
8
+ import { useNavigation, useURLLocation } from "./route-hooks.ts";
9
+ import type { AppRoute } from "./route-types.ts";
10
+ import type { AnchorHTMLAttributes, MouseEvent, ReactNode } from "./runtime.ts";
11
+ import { createElement } from "./runtime.ts";
12
+
13
+ export type { NavigateOptions } from "../core/index.ts";
14
+
15
+ export type { NavigationType } from "../core/navigation.ts";
16
+
17
+ /** Navigate to a URL or move through browser history using a numeric offset. */
18
+ export type NavigateFunction = (
19
+ to: string | number | Partial<Pick<URL, "pathname" | "search" | "hash">>,
20
+ options?: NavigateOptions,
21
+ ) => void;
22
+
23
+ /** Anchor properties and router navigation options, including document reload and prefetch controls. */
24
+ export interface LinkProps
25
+ extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
26
+ to?: string;
27
+ href?: string;
28
+ replace?: boolean;
29
+ prefetch?: string;
30
+ state?: unknown;
31
+ preventScrollReset?: boolean;
32
+ viewTransition?: boolean;
33
+ reloadDocument?: boolean;
34
+ }
35
+
36
+ /** Active and pending flags supplied to NavLink render callbacks. */
37
+ export interface NavLinkRenderProps {
38
+ isActive: boolean;
39
+ isPending: boolean;
40
+ }
41
+
42
+ /** Link options with matching rules and state-aware class or children callbacks. */
43
+ export interface NavLinkProps
44
+ extends Omit<LinkProps, "className" | "children"> {
45
+ end?: boolean;
46
+ className?: string | ((state: NavLinkRenderProps) => string);
47
+ children?: ReactNode | ((state: NavLinkRenderProps) => ReactNode);
48
+ }
49
+
50
+ /** Render an anchor with same-origin router navigation and optional intent prefetching. */
51
+ export function Link(options: LinkProps): ReactNode {
52
+ const {
53
+ href: hrefProp,
54
+ to = hrefProp ?? "/",
55
+ replace,
56
+ preventScrollReset,
57
+ viewTransition,
58
+ prefetch,
59
+ state,
60
+ reloadDocument,
61
+ ...props
62
+ } = options;
63
+ const router = useRouter();
64
+ const current = new URL(useSnapshot("url"));
65
+ const destination = resolveApplicationURL(new URL(to, current));
66
+ const internal = destination.origin === current.origin;
67
+ const preload = () => {
68
+ const route = matchRoutes(router.routes, destination.pathname)?.route as
69
+ | AppRoute
70
+ | undefined;
71
+
72
+ void route?.preload?.().catch(() => {});
73
+ };
74
+
75
+ if (!reloadDocument && prefetch !== "none" && internal) {
76
+ for (const name of ["onPointerEnter", "onFocus", "onTouchStart"] as const) {
77
+ const handler = props[name] as ((event: unknown) => void) | undefined;
78
+
79
+ props[name] = (event) => {
80
+ handler?.(event);
81
+ preload();
82
+ };
83
+ }
84
+ }
85
+
86
+ return createElement("a", {
87
+ ...props,
88
+ href: destination.href,
89
+ onClick(event: MouseEvent<HTMLAnchorElement>) {
90
+ props.onClick?.(event);
91
+
92
+ if (reloadDocument || !navigationClick(event, event.currentTarget))
93
+ return;
94
+
95
+ if (!internal) {
96
+ handoffDocument(event.nativeEvent, destination);
97
+
98
+ return;
99
+ }
100
+
101
+ event.preventDefault();
102
+ void router.navigate(destination.href, options);
103
+ },
104
+ });
105
+ }
106
+
107
+ /** Render a Link with active-route state and aria-current for the matching destination. */
108
+ export function NavLink({
109
+ href,
110
+ to = href ?? "/",
111
+ end,
112
+ className,
113
+ children,
114
+ ...props
115
+ }: NavLinkProps): ReactNode {
116
+ const location = useURLLocation();
117
+ const destination = resolveApplicationURL(new URL(to, location.href));
118
+ const target = destination.pathname;
119
+ const isActive =
120
+ destination.origin === location.origin &&
121
+ (location.pathname === target ||
122
+ (!end && target !== "/" && location.pathname.startsWith(`${target}/`)));
123
+ const pending = useNavigation().location;
124
+ const isPending =
125
+ !!pending &&
126
+ pending.origin === destination.origin &&
127
+ (pending.pathname === target ||
128
+ (!end && target !== "/" && pending.pathname.startsWith(`${target}/`)));
129
+ const state = { isActive, isPending };
130
+
131
+ return createElement(Link, {
132
+ ...props,
133
+ to,
134
+ className: typeof className === "function" ? className(state) : className,
135
+ ...(isActive && { "aria-current": "page" }),
136
+ children: typeof children === "function" ? children(state) : children,
137
+ });
138
+ }
139
+
140
+ export {
141
+ useNavigate,
142
+ useNavigationType,
143
+ useSearchParams,
144
+ useURLLocation,
145
+ } from "./route-hooks.ts";
@@ -0,0 +1,48 @@
1
+ import { useRouter, useSnapshot } from "./context.ts";
2
+ import { Routes } from "./routes.ts";
3
+ import {
4
+ createContext,
5
+ createElement,
6
+ type ReactNode,
7
+ useContext,
8
+ } from "./runtime.ts";
9
+
10
+ const OutletContext = createContext<unknown>(undefined);
11
+
12
+ /** Optional children and context shared with descendants through useOutletContext. */
13
+ export interface OutletProps {
14
+ context?: unknown;
15
+ children?: ReactNode;
16
+ }
17
+
18
+ /** Render the active route under a layout's shared context. */
19
+ export function Outlet({ context, children }: OutletProps): ReactNode {
20
+ const inherited = useContext(OutletContext);
21
+ return createElement(
22
+ OutletContext.Provider,
23
+ { value: context ?? inherited },
24
+ children ??
25
+ createElement(Routes, {
26
+ fallback: createElement("p", { role: "status" }, "Loading…"),
27
+ }),
28
+ );
29
+ }
30
+
31
+ /** Read the nearest Outlet's context. */
32
+ export function useOutletContext<T>(): T {
33
+ return useContext(OutletContext) as T;
34
+ }
35
+
36
+ /** Read the active flat route match; this is not a nested layout match chain. */
37
+ export function useMatches(): {
38
+ id: string;
39
+ data: unknown;
40
+ params: import("../core/index.ts").Params;
41
+ }[] {
42
+ const router = useRouter();
43
+ const snapshot = useSnapshot();
44
+ const route = router.routes.find((item) => item.path === snapshot.path);
45
+ return [
46
+ { id: route?.id ?? "", data: snapshot.data, params: snapshot.params },
47
+ ];
48
+ }
@@ -0,0 +1,22 @@
1
+ import type { Router } from "../core/index.ts";
2
+ import type { AppRoute, RootModule } from "./route-types.ts";
3
+ import { createContext, useContext } from "./runtime.ts";
4
+
5
+ export interface ApplicationContext {
6
+ router: Router;
7
+ routes: AppRoute[];
8
+ root: unknown;
9
+ rootError?: boolean;
10
+ entry?: string;
11
+ streaming?: boolean;
12
+ initialData?: string;
13
+ rootModule?: RootModule;
14
+ }
15
+
16
+ export const Application = createContext<ApplicationContext | null>(null);
17
+
18
+ export function useApplication() {
19
+ const app = useContext(Application);
20
+ if (!app) throw new Error("Root data requires RouterApplication.");
21
+ return app;
22
+ }