@kasperrt/smolter 0.4.3 → 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 (112) 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/prepare-routes.ts +50 -0
  5. package/.generated/core/presentation.ts +1 -1
  6. package/.generated/preact/client.tsx +1 -26
  7. package/.generated/preact/deferred-hydration.ts +1 -98
  8. package/.generated/preact/index.ts +6 -1
  9. package/.generated/preact/navigation.tsx +13 -5
  10. package/.generated/preact/root-data.ts +1 -45
  11. package/.generated/preact/router.tsx +2 -48
  12. package/.generated/react/client.tsx +130 -0
  13. package/.generated/react/document.tsx +235 -0
  14. package/.generated/react/fetcher.ts +29 -7
  15. package/.generated/react/hooks.ts +20 -18
  16. package/.generated/react/index.ts +17 -1
  17. package/.generated/react/lazy-route.ts +116 -0
  18. package/.generated/react/link.ts +1 -38
  19. package/.generated/react/navigation-state.ts +13 -0
  20. package/.generated/react/navigation.ts +145 -0
  21. package/.generated/react/root-context.ts +22 -0
  22. package/.generated/react/route-hooks.ts +159 -0
  23. package/.generated/react/route-types.ts +91 -0
  24. package/.generated/react/router.ts +50 -0
  25. package/.generated/react/runtime.ts +1 -0
  26. package/.generated/server/application.tsx +18 -253
  27. package/.generated/server/dispatch.ts +87 -0
  28. package/.generated/server/index.ts +2 -0
  29. package/.generated/server/react.ts +34 -0
  30. package/.generated/server/render-application.ts +203 -0
  31. package/.generated/vite/application/lazy-files.ts +2 -1
  32. package/.generated/vite/application/vite.ts +12 -4
  33. package/.generated/vite/react.ts +11 -0
  34. package/CHANGELOG.md +10 -0
  35. package/README.md +41 -41
  36. package/dist/client/hash-history.d.ts +2 -0
  37. package/dist/client/hash-history.js +26 -0
  38. package/dist/client/hydration.d.ts +10 -0
  39. package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
  40. package/dist/client/root-data.d.ts +14 -0
  41. package/dist/{preact → client}/root-data.js +1 -1
  42. package/dist/core/prepare-routes.d.ts +7 -0
  43. package/dist/core/prepare-routes.js +28 -0
  44. package/dist/core/presentation.js +1 -1
  45. package/dist/preact/client.js +5 -26
  46. package/dist/preact/deferred-hydration.d.ts +1 -10
  47. package/dist/preact/document.js +2 -2
  48. package/dist/preact/index.d.ts +1 -1
  49. package/dist/preact/index.js +1 -1
  50. package/dist/preact/navigation-state.js +1 -1
  51. package/dist/preact/navigation.js +7 -5
  52. package/dist/preact/root-data.d.ts +1 -8
  53. package/dist/preact/route-hooks.js +1 -1
  54. package/dist/preact/router.d.ts +2 -2
  55. package/dist/preact/router.js +2 -26
  56. package/dist/react/client.d.ts +20 -0
  57. package/dist/react/client.js +86 -0
  58. package/dist/react/document.d.ts +25 -0
  59. package/dist/react/document.js +169 -0
  60. package/dist/react/fetcher.d.ts +8 -4
  61. package/dist/react/fetcher.js +12 -1
  62. package/dist/react/form.js +1 -1
  63. package/dist/react/hooks.d.ts +7 -6
  64. package/dist/react/index.d.ts +7 -1
  65. package/dist/react/index.js +6 -2
  66. package/dist/react/lazy-route.d.ts +22 -0
  67. package/dist/react/lazy-route.js +57 -0
  68. package/dist/react/link.d.ts +1 -12
  69. package/dist/react/navigation-state.d.ts +2 -0
  70. package/dist/react/navigation-state.js +9 -0
  71. package/dist/react/navigation.d.ts +33 -0
  72. package/dist/react/navigation.js +64 -0
  73. package/dist/react/root-context.d.ts +14 -0
  74. package/dist/react/root-context.js +10 -0
  75. package/dist/react/route-hooks.d.ts +34 -0
  76. package/dist/react/route-hooks.js +89 -0
  77. package/dist/react/route-types.d.ts +69 -0
  78. package/dist/react/router.d.ts +10 -0
  79. package/dist/react/router.js +22 -0
  80. package/dist/react/runtime.d.ts +1 -1
  81. package/dist/react/runtime.js +1 -1
  82. package/dist/server/application.d.ts +4 -8
  83. package/dist/server/application.js +9 -156
  84. package/dist/server/dispatch.d.ts +5 -0
  85. package/dist/server/dispatch.js +50 -0
  86. package/dist/server/index.d.ts +1 -0
  87. package/dist/server/index.js +2 -1
  88. package/dist/server/preact.js +2 -1
  89. package/dist/server/react.d.ts +10 -0
  90. package/dist/server/react.js +22 -0
  91. package/dist/server/render-application.d.ts +31 -0
  92. package/dist/server/render-application.js +118 -0
  93. package/dist/vite/application/lazy-files.d.ts +1 -1
  94. package/dist/vite/application/lazy-files.js +2 -2
  95. package/dist/vite/application/vite.d.ts +2 -1
  96. package/dist/vite/application/vite.js +12 -5
  97. package/dist/vite/react.d.ts +4 -0
  98. package/dist/vite/react.js +9 -1
  99. package/jsr.json +10 -6
  100. package/package.json +18 -10
  101. package/.generated/core/resource-types.ts +0 -48
  102. package/.generated/core/resources.ts +0 -446
  103. package/.generated/react/data.ts +0 -29
  104. package/.generated/react/resources/components.tsx +0 -416
  105. package/dist/core/resource-types.d.ts +0 -41
  106. package/dist/core/resources.d.ts +0 -21
  107. package/dist/core/resources.js +0 -347
  108. package/dist/react/data.d.ts +0 -9
  109. package/dist/react/data.js +0 -3
  110. package/dist/react/link.js +0 -27
  111. package/dist/react/resources/components.d.ts +0 -76
  112. package/dist/react/resources/components.js +0 -250
@@ -0,0 +1,169 @@
1
+ import { serializeData } from "../core/serialize.js";
2
+ import { useSnapshot } from "./context.js";
3
+ import { applicationEnvironment } from "../core/environment.js";
4
+ import { scrollPositions } from "../core/navigation.js";
5
+ import { useNavigationSnapshot } from "./navigation-state.js";
6
+ import { useApplication } from "./root-context.js";
7
+ import { Fragment, createElement, useEffect, useLayoutEffect, useRef, useState } from "react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ //#region .generated/react/document.tsx
10
+ /**
11
+ * React document metadata, resource links and hydration scripts.
12
+ *
13
+ * @module
14
+ */
15
+ /** @jsxImportSource react */
16
+ function renderMeta(item, index) {
17
+ if (typeof item.title === "string") return /* @__PURE__ */ jsx("title", { children: item.title }, index);
18
+ if ("script:ld+json" in item) return /* @__PURE__ */ jsx("script", {
19
+ type: "application/ld+json",
20
+ dangerouslySetInnerHTML: { __html: serializeData(item["script:ld+json"]) }
21
+ }, index);
22
+ const { tagName, ...props } = item;
23
+ return createElement(tagName === "link" ? "link" : "meta", {
24
+ ...props,
25
+ key: index
26
+ });
27
+ }
28
+ /** Render root or active-route metadata in the document head. */
29
+ function Meta() {
30
+ const { routes, rootModule } = useApplication();
31
+ const snapshot = useSnapshot();
32
+ const route = routes.find((item) => item.path === snapshot.path);
33
+ const args = {
34
+ loaderData: snapshot.data,
35
+ data: snapshot.data,
36
+ params: snapshot.params,
37
+ error: snapshot.error,
38
+ location: new URL(snapshot.url)
39
+ };
40
+ const values = route?.meta?.(args) ?? rootModule?.meta?.(args) ?? [];
41
+ useEffect(() => {
42
+ const title = values.find((item) => typeof item.title === "string")?.title;
43
+ if (typeof title === "string") document.title = title;
44
+ }, [values]);
45
+ return /* @__PURE__ */ jsx(Fragment, { children: values.map(renderMeta) });
46
+ }
47
+ /** Render stylesheet and other link descriptors from the application root. */
48
+ function Links() {
49
+ const { rootModule } = useApplication();
50
+ return /* @__PURE__ */ jsx(Fragment, { children: rootModule?.links?.().map((props, index) => /* @__PURE__ */ jsx("link", { ...props }, index)) });
51
+ }
52
+ /** Render the hydration snapshot and browser entry script with an optional CSP nonce. */
53
+ function Scripts({ nonce }) {
54
+ const { root, rootError, router, entry, streaming, initialData } = useApplication();
55
+ const [json] = useState(() => {
56
+ if (initialData !== void 0) return initialData;
57
+ if (streaming) return "";
58
+ const current = router.getSnapshot();
59
+ const snapshot = {
60
+ ...current,
61
+ error: current.error && {
62
+ ...current.error,
63
+ name: current.error.name,
64
+ message: current.error.message
65
+ }
66
+ };
67
+ return serializeData({
68
+ root,
69
+ snapshot,
70
+ ...rootError && { rootError }
71
+ });
72
+ });
73
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
74
+ /* @__PURE__ */ jsx("script", {
75
+ ...nonce && { nonce },
76
+ dangerouslySetInnerHTML: { __html: applicationEnvironment?.entryScript ?? "" }
77
+ }),
78
+ /* @__PURE__ */ jsx("script", {
79
+ id: "smolter-data",
80
+ type: "application/json",
81
+ ...nonce && { nonce },
82
+ dangerouslySetInnerHTML: { __html: json }
83
+ }),
84
+ entry && /* @__PURE__ */ jsx("script", {
85
+ type: "module",
86
+ async: initialData !== void 0,
87
+ src: entry,
88
+ ...nonce && { nonce }
89
+ })
90
+ ] });
91
+ }
92
+ /** Reset scroll after navigation unless the navigation requests preservation. */
93
+ function ScrollRestoration(_props) {
94
+ const { router } = useApplication();
95
+ const { url, state } = useSnapshot();
96
+ const navigation = useNavigationSnapshot(router);
97
+ const positions = scrollPositions(router);
98
+ const restoredLocation = useRef("");
99
+ useEffect(() => {
100
+ const previous = history.scrollRestoration;
101
+ history.scrollRestoration = "manual";
102
+ return () => {
103
+ history.scrollRestoration = previous;
104
+ };
105
+ }, []);
106
+ useLayoutEffect(() => {
107
+ if (state !== "idle") return;
108
+ if ((history.state?.key ?? "initial") !== navigation.key) return;
109
+ const locationKey = `${navigation.key}:${url}`;
110
+ if (restoredLocation.current === locationKey) return;
111
+ const frame = requestAnimationFrame(() => {
112
+ restoredLocation.current = locationKey;
113
+ const target = new URL(location.href);
114
+ const saved = navigation.type === "POP" && positions.get(navigation.key);
115
+ if (!saved && target.hash) {
116
+ document.getElementById(target.hash.slice(1))?.scrollIntoView();
117
+ return;
118
+ }
119
+ if (!saved && navigation.preventScrollReset) return;
120
+ const [left, top] = saved || [0, 0];
121
+ window.scrollTo({
122
+ left,
123
+ top,
124
+ behavior: "instant"
125
+ });
126
+ });
127
+ return () => cancelAnimationFrame(frame);
128
+ }, [
129
+ url,
130
+ state,
131
+ positions,
132
+ navigation.key,
133
+ navigation.type,
134
+ navigation.preventScrollReset
135
+ ]);
136
+ return null;
137
+ }
138
+ /** Render the server document and preserve client-managed theme classes on hydration. */
139
+ function Document(props) {
140
+ const { children, ...attributes } = props;
141
+ const className = String(props.className ?? "");
142
+ const previousClassName = useRef(className);
143
+ const lang = String(props.lang ?? "en");
144
+ const debug = String(props["data-debug"] ?? false);
145
+ useEffect(() => {
146
+ if (previousClassName.current !== className) {
147
+ const previous = previousClassName.current.split(/\s+/).filter(Boolean);
148
+ const next = className.split(/\s+/).filter(Boolean);
149
+ document.documentElement.classList.remove(...previous);
150
+ document.documentElement.classList.add(...next);
151
+ previousClassName.current = className;
152
+ }
153
+ document.documentElement.lang = lang;
154
+ document.documentElement.dataset.debug = debug;
155
+ }, [
156
+ className,
157
+ lang,
158
+ debug
159
+ ]);
160
+ const classes = typeof document !== "undefined" && previousClassName.current !== className ? [...document.documentElement.classList].filter((value) => !previousClassName.current.split(/\s+/).includes(value)).concat(className.split(/\s+/).filter(Boolean)).join(" ") : props.className;
161
+ return /* @__PURE__ */ jsx("html", {
162
+ ...attributes,
163
+ className: classes,
164
+ suppressHydrationWarning: true,
165
+ children
166
+ });
167
+ }
168
+ //#endregion
169
+ export { Document, Links, Meta, Scripts, ScrollRestoration };
@@ -2,11 +2,13 @@
2
2
  * React components and typed data hooks. Import from @kasperrt/smolter/react.
3
3
  * @module
4
4
  */
5
- import { type AnyDataFunction, type DataFunction, type DataOf, type FetcherSnapshot, type Result, type Route as RouteDefinition, type RouteModule, type SubmitOptions } from "../core/index.ts";
5
+ import { type AnyDataFunction, type DataFunction, type DataOf, type FetcherSnapshot, type Result, type Route as RouteDefinition, type RouteModule } from "../core/index.ts";
6
+ import { type Submission, type SubmissionOptions } from "../core/submission.ts";
6
7
  import { type FormProps } from "./form.ts";
8
+ import type { RouteData } from "./route-types.ts";
7
9
  import { type ComponentType } from "./runtime.ts";
8
10
  /** Action method and optional explicit route URL for a fetcher submission. */
9
- export interface FetcherSubmitOptions extends SubmitOptions {
11
+ export interface FetcherSubmitOptions extends SubmissionOptions {
10
12
  action?: string;
11
13
  }
12
14
  /** Typed independent request state, methods, and a bound Form component. */
@@ -14,9 +16,11 @@ export interface TypedFetcher<T> extends Omit<FetcherSnapshot, "data"> {
14
16
  data?: T;
15
17
  Form: ComponentType<FormProps>;
16
18
  load: (url?: string) => Promise<Result>;
17
- submit: (body: FormData, options?: FetcherSubmitOptions) => Promise<Result>;
19
+ submit: (body: Submission, options?: FetcherSubmitOptions) => Promise<Result>;
18
20
  }
19
21
  /** Create a fetcher for independent route reads and mutations; its data stays local. */
20
22
  export declare function useFetcher<T extends RouteModule>(route: RouteDefinition<T>): TypedFetcher<DataOf<Extract<T["action"] | T["clientAction"], AnyDataFunction>> | DataOf<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>>;
21
23
  /** Create a fetcher for independent route reads and mutations; its data stays local. */
22
- export declare function useFetcher<T extends AnyDataFunction = DataFunction>(): TypedFetcher<DataOf<T>>;
24
+ export declare function useFetcher<T = DataFunction>(): TypedFetcher<RouteData<T>>;
25
+ /** A fetcher with a bound form and inferred result data. */
26
+ export type FetcherWithComponents<T> = TypedFetcher<RouteData<T>>;
@@ -1,3 +1,4 @@
1
+ import { submissionBody, submissionSearch } from "../core/submission.js";
1
2
  import { createElement, useEffect, useMemo, useSyncExternalStore } from "./runtime.js";
2
3
  import { useRouter } from "./context.js";
3
4
  import { Form } from "./form.js";
@@ -20,7 +21,17 @@ function useFetcher(route) {
20
21
  fetcher: store
21
22
  }),
22
23
  load: (url) => store.load(url ?? target()),
23
- submit: (body, options) => store.submit(options?.action ?? target(), body, options)
24
+ submit: (body, options = {}) => {
25
+ const action = options.action ?? target();
26
+ const data = submissionBody(body, options.encType);
27
+ const method = options.method?.toUpperCase();
28
+ if (method === "GET") {
29
+ const url = new URL(action, router.getSnapshot().url);
30
+ url.search = submissionSearch(data);
31
+ return store.load(url.href);
32
+ }
33
+ return store.submit(action, data, { method });
34
+ }
24
35
  };
25
36
  }, [router, path]);
26
37
  useEffect(() => fetcher.dispose, [fetcher]);
@@ -1,6 +1,6 @@
1
+ import { submitForm } from "../core/submission.js";
1
2
  import { createElement } from "./runtime.js";
2
3
  import { useRouter } from "./context.js";
3
- import { submitForm } from "../core/submission.js";
4
4
  //#region .generated/react/form.ts
5
5
  /** Submit form data to a registered route without a document reload. */
6
6
  function Form(options) {
@@ -2,19 +2,20 @@
2
2
  * React components and typed data hooks. Import from @kasperrt/smolter/react.
3
3
  * @module
4
4
  */
5
- import type { AnyDataFunction, DataOf, RouteModule, Router, Snapshot } from "../core/index.ts";
5
+ import type { AnyDataFunction, RouteModule, Router, Snapshot } from "../core/index.ts";
6
+ import type { RouteData } from "./route-types.ts";
6
7
  /** A route module or loader function used for data inference. */
7
8
  export type LoaderSource = RouteModule | AnyDataFunction;
8
9
  /** Infer server and client loader data from a route module or function. */
9
- export type LoaderData<T> = T extends AnyDataFunction ? DataOf<T> : T extends RouteModule ? DataOf<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>> : unknown;
10
+ export type LoaderData<T> = T extends AnyDataFunction ? RouteData<T> : T extends RouteModule ? RouteData<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>> : RouteData<T>;
10
11
  /** Infer server and client action data from a route module or function. */
11
- export type ActionData<T> = T extends AnyDataFunction ? DataOf<T> : T extends RouteModule ? DataOf<Extract<T["action"] | T["clientAction"], AnyDataFunction>> : unknown;
12
+ export type ActionData<T> = T extends AnyDataFunction ? RouteData<T> : T extends RouteModule ? RouteData<Extract<T["action"] | T["clientAction"], AnyDataFunction>> : RouteData<T>;
12
13
  /** Pass a route for inference, or use useLoaderData<typeof loader>() in its module. */
13
- export declare function useLoaderData<T extends LoaderSource = RouteModule>(route?: T): LoaderData<T> | undefined;
14
+ export declare function useLoaderData<T = RouteModule>(route?: T & LoaderSource): LoaderData<T>;
14
15
  /** Read the last page action result, optionally inferring its type from a route. */
15
- export declare function useActionData<T extends LoaderSource = RouteModule>(route?: T): ActionData<T> | undefined;
16
+ export declare function useActionData<T = RouteModule>(route?: T & LoaderSource): ActionData<T> | undefined;
16
17
  /** Read path parameters for the active route. */
17
- export declare function useParams(): Snapshot["params"];
18
+ export declare function useParams<T extends Snapshot["params"] = Snapshot["params"]>(): T;
18
19
  /** Read the current path, search, and hash together with the navigate function. */
19
20
  export declare function useLocation(): readonly [string, Router["navigate"]];
20
21
  /** Get the function that revalidates the current loader subject to shouldRevalidate. */
@@ -5,10 +5,16 @@
5
5
  */
6
6
  export type * from "../core/index.ts";
7
7
  export { createDataRouter, createRouter, defineRoute, } from "../core/index.ts";
8
+ export type { ApplicationEnvironment } from "../core/environment.ts";
9
+ export * from "../core/responses.ts";
10
+ export { readSubmission, type Submission, type SubmissionOptions, } from "../core/submission.ts";
8
11
  export { RouterProvider, type RouterProviderProps, useRouter, } from "./context.ts";
9
12
  export * from "./fetcher.ts";
10
13
  export * from "./form.ts";
11
14
  export * from "./hooks.ts";
12
- export * from "./link.ts";
15
+ export * from "./navigation.ts";
13
16
  export { Outlet, type OutletProps, useMatches, useOutletContext, } from "./outlet.ts";
17
+ export { useNavigation, useRootData, useRouteError, useRouteLoaderData, useSubmit, } from "./route-hooks.ts";
18
+ export * from "./route-types.ts";
19
+ export { type ApplicationProps, RouterApplication } from "./router.ts";
14
20
  export * from "./routes.ts";
@@ -1,11 +1,15 @@
1
1
  import { defineRoute } from "../core/match.js";
2
2
  import { createRouter } from "../core/navigation-router.js";
3
3
  import { createDataRouter } from "../core/router.js";
4
+ import { data, isRouteErrorResponse, normalizeResult, redirect, resolveData } from "../core/responses.js";
5
+ import { readSubmission } from "../core/submission.js";
4
6
  import { RouterProvider, useRouter } from "./context.js";
5
7
  import { Form } from "./form.js";
6
8
  import { useFetcher } from "./fetcher.js";
7
9
  import { useActionData, useLoaderData, useLocation, useParams, useRevalidator } from "./hooks.js";
8
- import { Link } from "./link.js";
10
+ import { useNavigate, useNavigation, useNavigationType, useRootData, useRouteError, useRouteLoaderData, useSearchParams, useSubmit, useURLLocation } from "./route-hooks.js";
11
+ import { Link, NavLink } from "./navigation.js";
9
12
  import { Routes } from "./routes.js";
10
13
  import { Outlet, useMatches, useOutletContext } from "./outlet.js";
11
- export { Form, Link, Outlet, RouterProvider, Routes, createDataRouter, createRouter, defineRoute, useActionData, useFetcher, useLoaderData, useLocation, useMatches, useOutletContext, useParams, useRevalidator, useRouter };
14
+ import { RouterApplication } from "./router.js";
15
+ export { Form, Link, NavLink, Outlet, RouterApplication, RouterProvider, Routes, createDataRouter, createRouter, data, defineRoute, isRouteErrorResponse, normalizeResult, readSubmission, redirect, resolveData, useActionData, useFetcher, useLoaderData, useLocation, useMatches, useNavigate, useNavigation, useNavigationType, useOutletContext, useParams, useRevalidator, useRootData, useRouteError, useRouteLoaderData, useRouter, useSearchParams, useSubmit, useURLLocation };
@@ -0,0 +1,22 @@
1
+ import type { RouteModule } from "../core/index.ts";
2
+ import type { AppRoute } from "./route-types.ts";
3
+ import { type ComponentType } from "./runtime.ts";
4
+ /** Lazy route path, server transport endpoint and available handler capabilities. */
5
+ export interface LazyRouteOptions {
6
+ path: string;
7
+ dataEndpoint?: string;
8
+ id?: string;
9
+ loader?: boolean;
10
+ action?: boolean;
11
+ clientAction?: boolean;
12
+ meta?: boolean;
13
+ shouldRevalidate?: boolean;
14
+ ErrorBoundary?: boolean;
15
+ }
16
+ type LoadedRoute = RouteModule & Pick<AppRoute, "meta">;
17
+ interface LazyAppRoute extends AppRoute {
18
+ resolveComponent: () => ComponentType | undefined;
19
+ }
20
+ /** Create a route that imports its module on demand and shares the import with intent prefetching. */
21
+ export declare function lazyRoute(options: LazyRouteOptions, importer: () => Promise<LoadedRoute>): LazyAppRoute;
22
+ export {};
@@ -0,0 +1,57 @@
1
+ import { createElement, useSyncExternalStore } from "./runtime.js";
2
+ import { useRouter } from "./context.js";
3
+ import { serverRoute } from "../client/index.js";
4
+ //#region .generated/react/lazy-route.ts
5
+ /**
6
+ * On-demand React route modules with shared intent prefetching.
7
+ *
8
+ * @module
9
+ */
10
+ function LazyView() {
11
+ const router = useRouter();
12
+ const { path } = useSyncExternalStore(router.subscribe, router.getSnapshot, router.getSnapshot);
13
+ const Component = router.routes.find((item) => item.path === path)?.resolveComponent();
14
+ return Component && createElement(Component, {});
15
+ }
16
+ /** Create a route that imports its module on demand and shares the import with intent prefetching. */
17
+ function lazyRoute(options, importer) {
18
+ let module;
19
+ let pending;
20
+ function preload() {
21
+ pending ??= importer().then((loaded) => {
22
+ module = serverRoute(options.path, loaded, {
23
+ loader: options.loader,
24
+ action: options.action,
25
+ dataEndpoint: options.dataEndpoint
26
+ });
27
+ }).catch((error) => {
28
+ pending = void 0;
29
+ throw error;
30
+ });
31
+ return pending;
32
+ }
33
+ async function run(args, action = false) {
34
+ await preload();
35
+ if (args.signal.aborted) return /* @__PURE__ */ new Error("Navigation cancelled");
36
+ return (action ? module.clientAction ?? module.action : module.clientLoader ?? module.loader)?.(args);
37
+ }
38
+ function ErrorBoundary(props) {
39
+ const Component = module?.ErrorBoundary;
40
+ if (!Component) return props.error.message;
41
+ return createElement(Component, props);
42
+ }
43
+ return {
44
+ path: options.path,
45
+ id: options.id,
46
+ preload,
47
+ default: LazyView,
48
+ resolveComponent: () => module?.default,
49
+ clientLoader: (args) => run(args),
50
+ ...(options.action || options.clientAction) && { clientAction: (args) => run(args, true) },
51
+ ...options.meta && { meta: (args) => module?.meta?.(args) ?? [] },
52
+ ...options.shouldRevalidate && { shouldRevalidate: (args) => module?.shouldRevalidate?.(args) ?? true },
53
+ ...options.ErrorBoundary && { ErrorBoundary }
54
+ };
55
+ }
56
+ //#endregion
57
+ export { lazyRoute };
@@ -1,12 +1 @@
1
- /**
2
- * React components and typed data hooks. Import from @kasperrt/smolter/react.
3
- * @module
4
- */
5
- import { type AnchorHTMLAttributes, type ReactNode } from "./runtime.ts";
6
- /** Anchor attributes with internal navigation and optional history replacement. */
7
- export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
8
- href: string;
9
- replace?: boolean;
10
- }
11
- /** Render an anchor that handles internal navigation and preserves native modified clicks. */
12
- export declare function Link(options: LinkProps): ReactNode;
1
+ export { Link, type LinkProps } from "./navigation.ts";
@@ -0,0 +1,2 @@
1
+ import type { Router } from "../core/index.ts";
2
+ export declare function useNavigationSnapshot(router: Router): import("../core/navigation.ts").NavigationSnapshot;
@@ -0,0 +1,9 @@
1
+ import { useSyncExternalStore } from "./runtime.js";
2
+ import { navigationStore } from "../core/navigation.js";
3
+ //#region .generated/react/navigation-state.ts
4
+ function useNavigationSnapshot(router) {
5
+ const store = navigationStore(router);
6
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
7
+ }
8
+ //#endregion
9
+ export { useNavigationSnapshot };
@@ -0,0 +1,33 @@
1
+ import { type NavigateOptions } from "../core/index.ts";
2
+ import type { AnchorHTMLAttributes, ReactNode } from "./runtime.ts";
3
+ export type { NavigateOptions } from "../core/index.ts";
4
+ export type { NavigationType } from "../core/navigation.ts";
5
+ /** Navigate to a URL or move through browser history using a numeric offset. */
6
+ export type NavigateFunction = (to: string | number | Partial<Pick<URL, "pathname" | "search" | "hash">>, options?: NavigateOptions) => void;
7
+ /** Anchor properties and router navigation options, including document reload and prefetch controls. */
8
+ export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
9
+ to?: string;
10
+ href?: string;
11
+ replace?: boolean;
12
+ prefetch?: string;
13
+ state?: unknown;
14
+ preventScrollReset?: boolean;
15
+ viewTransition?: boolean;
16
+ reloadDocument?: boolean;
17
+ }
18
+ /** Active and pending flags supplied to NavLink render callbacks. */
19
+ export interface NavLinkRenderProps {
20
+ isActive: boolean;
21
+ isPending: boolean;
22
+ }
23
+ /** Link options with matching rules and state-aware class or children callbacks. */
24
+ export interface NavLinkProps extends Omit<LinkProps, "className" | "children"> {
25
+ end?: boolean;
26
+ className?: string | ((state: NavLinkRenderProps) => string);
27
+ children?: ReactNode | ((state: NavLinkRenderProps) => ReactNode);
28
+ }
29
+ /** Render an anchor with same-origin router navigation and optional intent prefetching. */
30
+ export declare function Link(options: LinkProps): ReactNode;
31
+ /** Render a Link with active-route state and aria-current for the matching destination. */
32
+ export declare function NavLink({ href, to, end, className, children, ...props }: NavLinkProps): ReactNode;
33
+ export { useNavigate, useNavigationType, useSearchParams, useURLLocation, } from "./route-hooks.ts";
@@ -0,0 +1,64 @@
1
+ import { matchRoutes } from "../core/match.js";
2
+ import { navigationClick } from "../core/url.js";
3
+ import { createElement } from "./runtime.js";
4
+ import { useRouter, useSnapshot } from "./context.js";
5
+ import { handoffDocument, resolveApplicationURL } from "../core/environment.js";
6
+ import { useNavigate, useNavigation, useNavigationType, useSearchParams, useURLLocation } from "./route-hooks.js";
7
+ //#region .generated/react/navigation.ts
8
+ /** Render an anchor with same-origin router navigation and optional intent prefetching. */
9
+ function Link(options) {
10
+ const { href: hrefProp, to = hrefProp ?? "/", replace, preventScrollReset, viewTransition, prefetch, state, reloadDocument, ...props } = options;
11
+ const router = useRouter();
12
+ const current = new URL(useSnapshot("url"));
13
+ const destination = resolveApplicationURL(new URL(to, current));
14
+ const internal = destination.origin === current.origin;
15
+ const preload = () => {
16
+ (matchRoutes(router.routes, destination.pathname)?.route)?.preload?.().catch(() => {});
17
+ };
18
+ if (!reloadDocument && prefetch !== "none" && internal) for (const name of [
19
+ "onPointerEnter",
20
+ "onFocus",
21
+ "onTouchStart"
22
+ ]) {
23
+ const handler = props[name];
24
+ props[name] = (event) => {
25
+ handler?.(event);
26
+ preload();
27
+ };
28
+ }
29
+ return createElement("a", {
30
+ ...props,
31
+ href: destination.href,
32
+ onClick(event) {
33
+ props.onClick?.(event);
34
+ if (reloadDocument || !navigationClick(event, event.currentTarget)) return;
35
+ if (!internal) {
36
+ handoffDocument(event.nativeEvent, destination);
37
+ return;
38
+ }
39
+ event.preventDefault();
40
+ router.navigate(destination.href, options);
41
+ }
42
+ });
43
+ }
44
+ /** Render a Link with active-route state and aria-current for the matching destination. */
45
+ function NavLink({ href, to = href ?? "/", end, className, children, ...props }) {
46
+ const location = useURLLocation();
47
+ const destination = resolveApplicationURL(new URL(to, location.href));
48
+ const target = destination.pathname;
49
+ const isActive = destination.origin === location.origin && (location.pathname === target || !end && target !== "/" && location.pathname.startsWith(`${target}/`));
50
+ const pending = useNavigation().location;
51
+ const state = {
52
+ isActive,
53
+ isPending: !!pending && pending.origin === destination.origin && (pending.pathname === target || !end && target !== "/" && pending.pathname.startsWith(`${target}/`))
54
+ };
55
+ return createElement(Link, {
56
+ ...props,
57
+ to,
58
+ className: typeof className === "function" ? className(state) : className,
59
+ ...isActive && { "aria-current": "page" },
60
+ children: typeof children === "function" ? children(state) : children
61
+ });
62
+ }
63
+ //#endregion
64
+ export { Link, NavLink, useNavigate, useNavigationType, useSearchParams, useURLLocation };
@@ -0,0 +1,14 @@
1
+ import type { Router } from "../core/index.ts";
2
+ import type { AppRoute, RootModule } from "./route-types.ts";
3
+ export interface ApplicationContext {
4
+ router: Router;
5
+ routes: AppRoute[];
6
+ root: unknown;
7
+ rootError?: boolean;
8
+ entry?: string;
9
+ streaming?: boolean;
10
+ initialData?: string;
11
+ rootModule?: RootModule;
12
+ }
13
+ export declare const Application: import("react").Context<ApplicationContext | null>;
14
+ export declare function useApplication(): ApplicationContext;
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from "./runtime.js";
2
+ //#region .generated/react/root-context.ts
3
+ var Application = createContext(null);
4
+ function useApplication() {
5
+ const app = useContext(Application);
6
+ if (!app) throw new Error("Root data requires RouterApplication.");
7
+ return app;
8
+ }
9
+ //#endregion
10
+ export { Application, useApplication };
@@ -0,0 +1,34 @@
1
+ import type { NavigateOptions } from "../core/index.ts";
2
+ import type { NavigationType } from "../core/navigation.ts";
3
+ import { type Submission, type SubmissionOptions } from "../core/submission.ts";
4
+ import { useSnapshot } from "./context.ts";
5
+ import type { NavigateFunction } from "./navigation.ts";
6
+ import type { RouteData } from "./route-types.ts";
7
+ /** Read root loader data from the application context. */
8
+ export declare function useRootData<T = unknown>(): RouteData<T>;
9
+ /** Read root data or the active route's data by its manifest identifier. */
10
+ export declare function useRouteLoaderData<T = unknown>(id: string): RouteData<T> | undefined;
11
+ export { useSnapshot };
12
+ /** Read the current route error for an error boundary. */
13
+ export declare function useRouteError(): Error | undefined;
14
+ /** Read the current page navigation or submission state. */
15
+ export declare function useNavigation(): {
16
+ state: "idle" | "loading" | "submitting";
17
+ location: URL | undefined;
18
+ };
19
+ /** Return a function that submits form data through the current router. */
20
+ export declare function useSubmit(): (value: Submission, options?: SubmissionOptions) => Promise<void> | Promise<import("../core/index.ts").Result<unknown>>;
21
+ /** Read the current router location as a URL. */
22
+ export declare function useURLLocation(): URL & {
23
+ state: unknown;
24
+ key: string;
25
+ };
26
+ /** Return the application navigation function, including history offsets and relative destinations. */
27
+ export declare function useNavigate(): NavigateFunction;
28
+ /** Read URL search parameters and update them through router navigation. */
29
+ export declare function useSearchParams(): [
30
+ URLSearchParams,
31
+ (value: URLSearchParams | Record<string, string>, options?: NavigateOptions) => void
32
+ ];
33
+ /** Read the last browser history operation. */
34
+ export declare function useNavigationType(): NavigationType;
@@ -0,0 +1,89 @@
1
+ import { submissionBody, submissionSearch } from "../core/submission.js";
2
+ import { useCallback, useMemo } from "./runtime.js";
3
+ import { useRouter, useSnapshot } from "./context.js";
4
+ import { pendingLocation } from "../core/presentation.js";
5
+ import { useNavigationSnapshot } from "./navigation-state.js";
6
+ import { useApplication } from "./root-context.js";
7
+ //#region .generated/react/route-hooks.ts
8
+ /** Read root loader data from the application context. */
9
+ function useRootData() {
10
+ return useApplication().root;
11
+ }
12
+ /** Read root data or the active route's data by its manifest identifier. */
13
+ function useRouteLoaderData(id) {
14
+ const app = useApplication();
15
+ const snapshot = useSnapshot();
16
+ if (id === "root") return app.root;
17
+ const route = app.routes.find((item) => item.id === id);
18
+ if (!route || route.path !== snapshot.path) return void 0;
19
+ return snapshot.data;
20
+ }
21
+ /** Read the current route error for an error boundary. */
22
+ function useRouteError() {
23
+ return useSnapshot("error");
24
+ }
25
+ /** Read the current page navigation or submission state. */
26
+ function useNavigation() {
27
+ const router = useRouter();
28
+ const snapshot = useSnapshot();
29
+ const location = pendingLocation(router);
30
+ return useMemo(() => ({
31
+ state: snapshot.state,
32
+ location
33
+ }), [snapshot.state, location?.href]);
34
+ }
35
+ /** Return a function that submits form data through the current router. */
36
+ function useSubmit() {
37
+ const router = useRouter();
38
+ return useCallback((value, options = {}) => {
39
+ const body = submissionBody(value, options.encType);
40
+ const method = options.method?.toUpperCase();
41
+ const target = options.action ?? router.getSnapshot().url;
42
+ if (method === "GET") {
43
+ const url = new URL(target, router.getSnapshot().url);
44
+ url.search = submissionSearch(body);
45
+ return router.navigate(url.href, options);
46
+ }
47
+ return router.submit(target, body, { method });
48
+ }, [router]);
49
+ }
50
+ /** Read the current router location as a URL. */
51
+ function useURLLocation() {
52
+ const href = useSnapshot("url");
53
+ const snapshot = useNavigationSnapshot(useRouter());
54
+ return useMemo(() => Object.assign(new URL(href), {
55
+ state: snapshot.state,
56
+ key: snapshot.key
57
+ }), [
58
+ href,
59
+ snapshot.state,
60
+ snapshot.key
61
+ ]);
62
+ }
63
+ /** Return the application navigation function, including history offsets and relative destinations. */
64
+ function useNavigate() {
65
+ const router = useRouter();
66
+ return useCallback((to, options) => {
67
+ if (typeof to === "number") {
68
+ window.history.go(to);
69
+ return;
70
+ }
71
+ const current = new URL(router.getSnapshot().url);
72
+ const target = typeof to === "string" ? to : `${to.pathname ?? current.pathname}${to.search ?? current.search}${to.hash ?? current.hash}`;
73
+ router.navigate(target, options);
74
+ }, [router]);
75
+ }
76
+ /** Read URL search parameters and update them through router navigation. */
77
+ function useSearchParams() {
78
+ const location = useURLLocation();
79
+ const navigate = useNavigate();
80
+ return [useMemo(() => new URLSearchParams(location.search), [location.search]), useCallback((value, options) => {
81
+ navigate(`${location.pathname}?${new URLSearchParams(value)}`, options);
82
+ }, [location.pathname, navigate])];
83
+ }
84
+ /** Read the last browser history operation. */
85
+ function useNavigationType() {
86
+ return useNavigationSnapshot(useRouter()).type;
87
+ }
88
+ //#endregion
89
+ export { useNavigate, useNavigation, useNavigationType, useRootData, useRouteError, useRouteLoaderData, useSearchParams, useSnapshot, useSubmit, useURLLocation };